Files
22
Total Lines
4662
Coverage
63.5%
630 / 992 lines
Actions
55.6%
lib/openzeppelin-contracts/contracts/access/Ownable.sol
Lines covered: 5 / 9 (55.6%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable.sol) |
||
| 3 | |||
| 4 |
pragma solidity ^0.8.20; |
||
| 5 | |||
| 6 |
import {Context} from "../utils/Context.sol";
|
||
| 7 | |||
| 8 |
/** |
||
| 9 |
* @dev Contract module which provides a basic access control mechanism, where |
||
| 10 |
* there is an account (an owner) that can be granted exclusive access to |
||
| 11 |
* specific functions. |
||
| 12 |
* |
||
| 13 |
* The initial owner is set to the address provided by the deployer. This can |
||
| 14 |
* later be changed with {transferOwnership}.
|
||
| 15 |
* |
||
| 16 |
* This module is used through inheritance. It will make available the modifier |
||
| 17 |
* `onlyOwner`, which can be applied to your functions to restrict their use to |
||
| 18 |
* the owner. |
||
| 19 |
*/ |
||
| 20 |
abstract contract Ownable is Context {
|
||
| 21 |
address private _owner; |
||
| 22 | |||
| 23 |
/** |
||
| 24 |
* @dev The caller account is not authorized to perform an operation. |
||
| 25 |
*/ |
||
| 26 |
error OwnableUnauthorizedAccount(address account); |
||
| 27 | |||
| 28 |
/** |
||
| 29 |
* @dev The owner is not a valid owner account. (eg. `address(0)`) |
||
| 30 |
*/ |
||
| 31 |
error OwnableInvalidOwner(address owner); |
||
| 32 | |||
| 33 |
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); |
||
| 34 | |||
| 35 |
/** |
||
| 36 |
* @dev Initializes the contract setting the address provided by the deployer as the initial owner. |
||
| 37 |
*/ |
||
| 38 |
constructor(address initialOwner) {
|
||
| 39 |
✓ 1
|
if (initialOwner == address(0)) {
|
|
| 40 |
revert OwnableInvalidOwner(address(0)); |
||
| 41 |
} |
||
| 42 |
_transferOwnership(initialOwner); |
||
| 43 |
} |
||
| 44 | |||
| 45 |
/** |
||
| 46 |
* @dev Throws if called by any account other than the owner. |
||
| 47 |
*/ |
||
| 48 |
✓ 82.9K
|
modifier onlyOwner() {
|
|
| 49 |
_checkOwner(); |
||
| 50 |
_; |
||
| 51 |
} |
||
| 52 | |||
| 53 |
/** |
||
| 54 |
* @dev Returns the address of the current owner. |
||
| 55 |
*/ |
||
| 56 |
function owner() public view virtual returns (address) {
|
||
| 57 |
✓ 135.5K
|
return _owner; |
|
| 58 |
} |
||
| 59 | |||
| 60 |
/** |
||
| 61 |
* @dev Throws if the sender is not the owner. |
||
| 62 |
*/ |
||
| 63 |
function _checkOwner() internal view virtual {
|
||
| 64 |
✓ 135.5K
|
if (owner() != _msgSender()) {
|
|
| 65 |
revert OwnableUnauthorizedAccount(_msgSender()); |
||
| 66 |
} |
||
| 67 |
} |
||
| 68 | |||
| 69 |
/** |
||
| 70 |
* @dev Leaves the contract without owner. It will not be possible to call |
||
| 71 |
* `onlyOwner` functions. Can only be called by the current owner. |
||
| 72 |
* |
||
| 73 |
* NOTE: Renouncing ownership will leave the contract without an owner, |
||
| 74 |
* thereby disabling any functionality that is only available to the owner. |
||
| 75 |
*/ |
||
| 76 |
function renounceOwnership() public virtual onlyOwner {
|
||
| 77 |
_transferOwnership(address(0)); |
||
| 78 |
} |
||
| 79 | |||
| 80 |
/** |
||
| 81 |
* @dev Transfers ownership of the contract to a new account (`newOwner`). |
||
| 82 |
* Can only be called by the current owner. |
||
| 83 |
*/ |
||
| 84 |
function transferOwnership(address newOwner) public virtual onlyOwner {
|
||
| 85 |
if (newOwner == address(0)) {
|
||
| 86 |
revert OwnableInvalidOwner(address(0)); |
||
| 87 |
} |
||
| 88 |
_transferOwnership(newOwner); |
||
| 89 |
} |
||
| 90 | |||
| 91 |
/** |
||
| 92 |
* @dev Transfers ownership of the contract to a new account (`newOwner`). |
||
| 93 |
* Internal function without access restriction. |
||
| 94 |
*/ |
||
| 95 |
function _transferOwnership(address newOwner) internal virtual {
|
||
| 96 |
address oldOwner = _owner; |
||
| 97 |
_owner = newOwner; |
||
| 98 |
✓ 1
|
emit OwnershipTransferred(oldOwner, newOwner); |
|
| 99 |
} |
||
| 100 |
} |
||
| 101 |
0.0%
lib/openzeppelin-contracts/contracts/interfaces/IERC1363.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC1363.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.6.2; |
||
| 5 | |||
| 6 |
import {IERC20} from "./IERC20.sol";
|
||
| 7 |
import {IERC165} from "./IERC165.sol";
|
||
| 8 | |||
| 9 |
/** |
||
| 10 |
* @title IERC1363 |
||
| 11 |
* @dev Interface of the ERC-1363 standard as defined in the https://eips.ethereum.org/EIPS/eip-1363[ERC-1363]. |
||
| 12 |
* |
||
| 13 |
* Defines an extension interface for ERC-20 tokens that supports executing code on a recipient contract |
||
| 14 |
* after `transfer` or `transferFrom`, or code on a spender contract after `approve`, in a single transaction. |
||
| 15 |
*/ |
||
| 16 |
interface IERC1363 is IERC20, IERC165 {
|
||
| 17 |
/* |
||
| 18 |
* Note: the ERC-165 identifier for this interface is 0xb0202a11. |
||
| 19 |
* 0xb0202a11 === |
||
| 20 |
* bytes4(keccak256('transferAndCall(address,uint256)')) ^
|
||
| 21 |
* bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^
|
||
| 22 |
* bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^
|
||
| 23 |
* bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^
|
||
| 24 |
* bytes4(keccak256('approveAndCall(address,uint256)')) ^
|
||
| 25 |
* bytes4(keccak256('approveAndCall(address,uint256,bytes)'))
|
||
| 26 |
*/ |
||
| 27 | |||
| 28 |
/** |
||
| 29 |
* @dev Moves a `value` amount of tokens from the caller's account to `to` |
||
| 30 |
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||
| 31 |
* @param to The address which you want to transfer to. |
||
| 32 |
* @param value The amount of tokens to be transferred. |
||
| 33 |
* @return A boolean value indicating whether the operation succeeded unless throwing. |
||
| 34 |
*/ |
||
| 35 |
function transferAndCall(address to, uint256 value) external returns (bool); |
||
| 36 | |||
| 37 |
/** |
||
| 38 |
* @dev Moves a `value` amount of tokens from the caller's account to `to` |
||
| 39 |
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||
| 40 |
* @param to The address which you want to transfer to. |
||
| 41 |
* @param value The amount of tokens to be transferred. |
||
| 42 |
* @param data Additional data with no specified format, sent in call to `to`. |
||
| 43 |
* @return A boolean value indicating whether the operation succeeded unless throwing. |
||
| 44 |
*/ |
||
| 45 |
function transferAndCall(address to, uint256 value, bytes calldata data) external returns (bool); |
||
| 46 | |||
| 47 |
/** |
||
| 48 |
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism |
||
| 49 |
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||
| 50 |
* @param from The address which you want to send tokens from. |
||
| 51 |
* @param to The address which you want to transfer to. |
||
| 52 |
* @param value The amount of tokens to be transferred. |
||
| 53 |
* @return A boolean value indicating whether the operation succeeded unless throwing. |
||
| 54 |
*/ |
||
| 55 |
function transferFromAndCall(address from, address to, uint256 value) external returns (bool); |
||
| 56 | |||
| 57 |
/** |
||
| 58 |
* @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism |
||
| 59 |
* and then calls {IERC1363Receiver-onTransferReceived} on `to`.
|
||
| 60 |
* @param from The address which you want to send tokens from. |
||
| 61 |
* @param to The address which you want to transfer to. |
||
| 62 |
* @param value The amount of tokens to be transferred. |
||
| 63 |
* @param data Additional data with no specified format, sent in call to `to`. |
||
| 64 |
* @return A boolean value indicating whether the operation succeeded unless throwing. |
||
| 65 |
*/ |
||
| 66 |
function transferFromAndCall(address from, address to, uint256 value, bytes calldata data) external returns (bool); |
||
| 67 | |||
| 68 |
/** |
||
| 69 |
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the |
||
| 70 |
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||
| 71 |
* @param spender The address which will spend the funds. |
||
| 72 |
* @param value The amount of tokens to be spent. |
||
| 73 |
* @return A boolean value indicating whether the operation succeeded unless throwing. |
||
| 74 |
*/ |
||
| 75 |
function approveAndCall(address spender, uint256 value) external returns (bool); |
||
| 76 | |||
| 77 |
/** |
||
| 78 |
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the |
||
| 79 |
* caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`.
|
||
| 80 |
* @param spender The address which will spend the funds. |
||
| 81 |
* @param value The amount of tokens to be spent. |
||
| 82 |
* @param data Additional data with no specified format, sent in call to `spender`. |
||
| 83 |
* @return A boolean value indicating whether the operation succeeded unless throwing. |
||
| 84 |
*/ |
||
| 85 |
function approveAndCall(address spender, uint256 value, bytes calldata data) external returns (bool); |
||
| 86 |
} |
||
| 87 |
0.0%
lib/openzeppelin-contracts/contracts/interfaces/IERC165.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC165.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.4.16; |
||
| 5 | |||
| 6 |
import {IERC165} from "../utils/introspection/IERC165.sol";
|
||
| 7 |
0.0%
lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.4.0) (interfaces/IERC20.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.4.16; |
||
| 5 | |||
| 6 |
import {IERC20} from "../token/ERC20/IERC20.sol";
|
||
| 7 |
0.0%
lib/openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.5.0) (interfaces/draft-IERC6093.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.8.4; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev Standard ERC-20 Errors |
||
| 8 |
* Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-20 tokens. |
||
| 9 |
*/ |
||
| 10 |
interface IERC20Errors {
|
||
| 11 |
/** |
||
| 12 |
* @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. |
||
| 13 |
* @param sender Address whose tokens are being transferred. |
||
| 14 |
* @param balance Current balance for the interacting account. |
||
| 15 |
* @param needed Minimum amount required to perform a transfer. |
||
| 16 |
*/ |
||
| 17 |
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); |
||
| 18 | |||
| 19 |
/** |
||
| 20 |
* @dev Indicates a failure with the token `sender`. Used in transfers. |
||
| 21 |
* @param sender Address whose tokens are being transferred. |
||
| 22 |
*/ |
||
| 23 |
error ERC20InvalidSender(address sender); |
||
| 24 | |||
| 25 |
/** |
||
| 26 |
* @dev Indicates a failure with the token `receiver`. Used in transfers. |
||
| 27 |
* @param receiver Address to which tokens are being transferred. |
||
| 28 |
*/ |
||
| 29 |
error ERC20InvalidReceiver(address receiver); |
||
| 30 | |||
| 31 |
/** |
||
| 32 |
* @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. |
||
| 33 |
* @param spender Address that may be allowed to operate on tokens without being their owner. |
||
| 34 |
* @param allowance Amount of tokens a `spender` is allowed to operate with. |
||
| 35 |
* @param needed Minimum amount required to perform a transfer. |
||
| 36 |
*/ |
||
| 37 |
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); |
||
| 38 | |||
| 39 |
/** |
||
| 40 |
* @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. |
||
| 41 |
* @param approver Address initiating an approval operation. |
||
| 42 |
*/ |
||
| 43 |
error ERC20InvalidApprover(address approver); |
||
| 44 | |||
| 45 |
/** |
||
| 46 |
* @dev Indicates a failure with the `spender` to be approved. Used in approvals. |
||
| 47 |
* @param spender Address that may be allowed to operate on tokens without being their owner. |
||
| 48 |
*/ |
||
| 49 |
error ERC20InvalidSpender(address spender); |
||
| 50 |
} |
||
| 51 | |||
| 52 |
/** |
||
| 53 |
* @dev Standard ERC-721 Errors |
||
| 54 |
* Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-721 tokens. |
||
| 55 |
*/ |
||
| 56 |
interface IERC721Errors {
|
||
| 57 |
/** |
||
| 58 |
* @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in ERC-721. |
||
| 59 |
* Used in balance queries. |
||
| 60 |
* @param owner Address of the current owner of a token. |
||
| 61 |
*/ |
||
| 62 |
error ERC721InvalidOwner(address owner); |
||
| 63 | |||
| 64 |
/** |
||
| 65 |
* @dev Indicates a `tokenId` whose `owner` is the zero address. |
||
| 66 |
* @param tokenId Identifier number of a token. |
||
| 67 |
*/ |
||
| 68 |
error ERC721NonexistentToken(uint256 tokenId); |
||
| 69 | |||
| 70 |
/** |
||
| 71 |
* @dev Indicates an error related to the ownership over a particular token. Used in transfers. |
||
| 72 |
* @param sender Address whose tokens are being transferred. |
||
| 73 |
* @param tokenId Identifier number of a token. |
||
| 74 |
* @param owner Address of the current owner of a token. |
||
| 75 |
*/ |
||
| 76 |
error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); |
||
| 77 | |||
| 78 |
/** |
||
| 79 |
* @dev Indicates a failure with the token `sender`. Used in transfers. |
||
| 80 |
* @param sender Address whose tokens are being transferred. |
||
| 81 |
*/ |
||
| 82 |
error ERC721InvalidSender(address sender); |
||
| 83 | |||
| 84 |
/** |
||
| 85 |
* @dev Indicates a failure with the token `receiver`. Used in transfers. |
||
| 86 |
* @param receiver Address to which tokens are being transferred. |
||
| 87 |
*/ |
||
| 88 |
error ERC721InvalidReceiver(address receiver); |
||
| 89 | |||
| 90 |
/** |
||
| 91 |
* @dev Indicates a failure with the `operator`’s approval. Used in transfers. |
||
| 92 |
* @param operator Address that may be allowed to operate on tokens without being their owner. |
||
| 93 |
* @param tokenId Identifier number of a token. |
||
| 94 |
*/ |
||
| 95 |
error ERC721InsufficientApproval(address operator, uint256 tokenId); |
||
| 96 | |||
| 97 |
/** |
||
| 98 |
* @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. |
||
| 99 |
* @param approver Address initiating an approval operation. |
||
| 100 |
*/ |
||
| 101 |
error ERC721InvalidApprover(address approver); |
||
| 102 | |||
| 103 |
/** |
||
| 104 |
* @dev Indicates a failure with the `operator` to be approved. Used in approvals. |
||
| 105 |
* @param operator Address that may be allowed to operate on tokens without being their owner. |
||
| 106 |
*/ |
||
| 107 |
error ERC721InvalidOperator(address operator); |
||
| 108 |
} |
||
| 109 | |||
| 110 |
/** |
||
| 111 |
* @dev Standard ERC-1155 Errors |
||
| 112 |
* Interface of the https://eips.ethereum.org/EIPS/eip-6093[ERC-6093] custom errors for ERC-1155 tokens. |
||
| 113 |
*/ |
||
| 114 |
interface IERC1155Errors {
|
||
| 115 |
/** |
||
| 116 |
* @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. |
||
| 117 |
* @param sender Address whose tokens are being transferred. |
||
| 118 |
* @param balance Current balance for the interacting account. |
||
| 119 |
* @param needed Minimum amount required to perform a transfer. |
||
| 120 |
* @param tokenId Identifier number of a token. |
||
| 121 |
*/ |
||
| 122 |
error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); |
||
| 123 | |||
| 124 |
/** |
||
| 125 |
* @dev Indicates a failure with the token `sender`. Used in transfers. |
||
| 126 |
* @param sender Address whose tokens are being transferred. |
||
| 127 |
*/ |
||
| 128 |
error ERC1155InvalidSender(address sender); |
||
| 129 | |||
| 130 |
/** |
||
| 131 |
* @dev Indicates a failure with the token `receiver`. Used in transfers. |
||
| 132 |
* @param receiver Address to which tokens are being transferred. |
||
| 133 |
*/ |
||
| 134 |
error ERC1155InvalidReceiver(address receiver); |
||
| 135 | |||
| 136 |
/** |
||
| 137 |
* @dev Indicates a failure with the `operator`’s approval. Used in transfers. |
||
| 138 |
* @param operator Address that may be allowed to operate on tokens without being their owner. |
||
| 139 |
* @param owner Address of the current owner of a token. |
||
| 140 |
*/ |
||
| 141 |
error ERC1155MissingApprovalForAll(address operator, address owner); |
||
| 142 | |||
| 143 |
/** |
||
| 144 |
* @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. |
||
| 145 |
* @param approver Address initiating an approval operation. |
||
| 146 |
*/ |
||
| 147 |
error ERC1155InvalidApprover(address approver); |
||
| 148 | |||
| 149 |
/** |
||
| 150 |
* @dev Indicates a failure with the `operator` to be approved. Used in approvals. |
||
| 151 |
* @param operator Address that may be allowed to operate on tokens without being their owner. |
||
| 152 |
*/ |
||
| 153 |
error ERC1155InvalidOperator(address operator); |
||
| 154 | |||
| 155 |
/** |
||
| 156 |
* @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. |
||
| 157 |
* Used in batch transfers. |
||
| 158 |
* @param idsLength Length of the array of token identifiers |
||
| 159 |
* @param valuesLength Length of the array of token amounts |
||
| 160 |
*/ |
||
| 161 |
error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); |
||
| 162 |
} |
||
| 163 |
100.0%
lib/openzeppelin-contracts/contracts/mocks/token/ERC20Mock.sol
Lines covered: 1 / 1 (100.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.20; |
||
| 3 | |||
| 4 |
import {ERC20} from "../../token/ERC20/ERC20.sol";
|
||
| 5 | |||
| 6 |
✓ 239.3K
|
contract ERC20Mock is ERC20 {
|
|
| 7 |
constructor() ERC20("ERC20Mock", "E20M") {}
|
||
| 8 | |||
| 9 |
function mint(address account, uint256 amount) external {
|
||
| 10 |
_mint(account, amount); |
||
| 11 |
} |
||
| 12 | |||
| 13 |
function burn(address account, uint256 amount) external {
|
||
| 14 |
_burn(account, amount); |
||
| 15 |
} |
||
| 16 |
} |
||
| 17 |
56.0%
lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol
Lines covered: 14 / 25 (56.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.5.0) (token/ERC20/ERC20.sol) |
||
| 3 | |||
| 4 |
pragma solidity ^0.8.20; |
||
| 5 | |||
| 6 |
import {IERC20} from "./IERC20.sol";
|
||
| 7 |
import {IERC20Metadata} from "./extensions/IERC20Metadata.sol";
|
||
| 8 |
import {Context} from "../../utils/Context.sol";
|
||
| 9 |
import {IERC20Errors} from "../../interfaces/draft-IERC6093.sol";
|
||
| 10 | |||
| 11 |
/** |
||
| 12 |
* @dev Implementation of the {IERC20} interface.
|
||
| 13 |
* |
||
| 14 |
* This implementation is agnostic to the way tokens are created. This means |
||
| 15 |
* that a supply mechanism has to be added in a derived contract using {_mint}.
|
||
| 16 |
* |
||
| 17 |
* TIP: For a detailed writeup see our guide |
||
| 18 |
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[How |
||
| 19 |
* to implement supply mechanisms]. |
||
| 20 |
* |
||
| 21 |
* The default value of {decimals} is 18. To change this, you should override
|
||
| 22 |
* this function so it returns a different value. |
||
| 23 |
* |
||
| 24 |
* We have followed general OpenZeppelin Contracts guidelines: functions revert |
||
| 25 |
* instead returning `false` on failure. This behavior is nonetheless |
||
| 26 |
* conventional and does not conflict with the expectations of ERC-20 |
||
| 27 |
* applications. |
||
| 28 |
*/ |
||
| 29 |
abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
|
||
| 30 |
mapping(address account => uint256) private _balances; |
||
| 31 | |||
| 32 |
mapping(address account => mapping(address spender => uint256)) private _allowances; |
||
| 33 | |||
| 34 |
uint256 private _totalSupply; |
||
| 35 | |||
| 36 |
string private _name; |
||
| 37 |
string private _symbol; |
||
| 38 | |||
| 39 |
/** |
||
| 40 |
* @dev Sets the values for {name} and {symbol}.
|
||
| 41 |
* |
||
| 42 |
* Both values are immutable: they can only be set once during construction. |
||
| 43 |
*/ |
||
| 44 |
constructor(string memory name_, string memory symbol_) {
|
||
| 45 |
✓ 1
|
_name = name_; |
|
| 46 |
_symbol = symbol_; |
||
| 47 |
} |
||
| 48 | |||
| 49 |
/** |
||
| 50 |
* @dev Returns the name of the token. |
||
| 51 |
*/ |
||
| 52 |
function name() public view virtual returns (string memory) {
|
||
| 53 |
return _name; |
||
| 54 |
} |
||
| 55 | |||
| 56 |
/** |
||
| 57 |
* @dev Returns the symbol of the token, usually a shorter version of the |
||
| 58 |
* name. |
||
| 59 |
*/ |
||
| 60 |
function symbol() public view virtual returns (string memory) {
|
||
| 61 |
return _symbol; |
||
| 62 |
} |
||
| 63 | |||
| 64 |
/** |
||
| 65 |
* @dev Returns the number of decimals used to get its user representation. |
||
| 66 |
* For example, if `decimals` equals `2`, a balance of `505` tokens should |
||
| 67 |
* be displayed to a user as `5.05` (`505 / 10 ** 2`). |
||
| 68 |
* |
||
| 69 |
* Tokens usually opt for a value of 18, imitating the relationship between |
||
| 70 |
* Ether and Wei. This is the default value returned by this function, unless |
||
| 71 |
* it's overridden. |
||
| 72 |
* |
||
| 73 |
* NOTE: This information is only used for _display_ purposes: it in |
||
| 74 |
* no way affects any of the arithmetic of the contract, including |
||
| 75 |
* {IERC20-balanceOf} and {IERC20-transfer}.
|
||
| 76 |
*/ |
||
| 77 |
function decimals() public view virtual returns (uint8) {
|
||
| 78 |
return 18; |
||
| 79 |
} |
||
| 80 | |||
| 81 |
/// @inheritdoc IERC20 |
||
| 82 |
function totalSupply() public view virtual returns (uint256) {
|
||
| 83 |
return _totalSupply; |
||
| 84 |
} |
||
| 85 | |||
| 86 |
/// @inheritdoc IERC20 |
||
| 87 |
function balanceOf(address account) public view virtual returns (uint256) {
|
||
| 88 |
return _balances[account]; |
||
| 89 |
} |
||
| 90 | |||
| 91 |
/** |
||
| 92 |
* @dev See {IERC20-transfer}.
|
||
| 93 |
* |
||
| 94 |
* Requirements: |
||
| 95 |
* |
||
| 96 |
* - `to` cannot be the zero address. |
||
| 97 |
* - the caller must have a balance of at least `value`. |
||
| 98 |
*/ |
||
| 99 |
function transfer(address to, uint256 value) public virtual returns (bool) {
|
||
| 100 |
address owner = _msgSender(); |
||
| 101 |
✓ 154.9K
|
_transfer(owner, to, value); |
|
| 102 |
return true; |
||
| 103 |
} |
||
| 104 | |||
| 105 |
/// @inheritdoc IERC20 |
||
| 106 |
function allowance(address owner, address spender) public view virtual returns (uint256) {
|
||
| 107 |
return _allowances[owner][spender]; |
||
| 108 |
} |
||
| 109 | |||
| 110 |
/** |
||
| 111 |
* @dev See {IERC20-approve}.
|
||
| 112 |
* |
||
| 113 |
* NOTE: If `value` is the maximum `uint256`, the allowance is not updated on |
||
| 114 |
* `transferFrom`. This is semantically equivalent to an infinite approval. |
||
| 115 |
* |
||
| 116 |
* Requirements: |
||
| 117 |
* |
||
| 118 |
* - `spender` cannot be the zero address. |
||
| 119 |
*/ |
||
| 120 |
function approve(address spender, uint256 value) public virtual returns (bool) {
|
||
| 121 |
address owner = _msgSender(); |
||
| 122 |
_approve(owner, spender, value); |
||
| 123 |
return true; |
||
| 124 |
} |
||
| 125 | |||
| 126 |
/** |
||
| 127 |
* @dev See {IERC20-transferFrom}.
|
||
| 128 |
* |
||
| 129 |
* Skips emitting an {Approval} event indicating an allowance update. This is not
|
||
| 130 |
* required by the ERC. See {xref-ERC20-_approve-address-address-uint256-bool-}[_approve].
|
||
| 131 |
* |
||
| 132 |
* NOTE: Does not update the allowance if the current allowance |
||
| 133 |
* is the maximum `uint256`. |
||
| 134 |
* |
||
| 135 |
* Requirements: |
||
| 136 |
* |
||
| 137 |
* - `from` and `to` cannot be the zero address. |
||
| 138 |
* - `from` must have a balance of at least `value`. |
||
| 139 |
* - the caller must have allowance for ``from``'s tokens of at least |
||
| 140 |
* `value`. |
||
| 141 |
*/ |
||
| 142 |
function transferFrom(address from, address to, uint256 value) public virtual returns (bool) {
|
||
| 143 |
address spender = _msgSender(); |
||
| 144 |
_spendAllowance(from, spender, value); |
||
| 145 |
✓ 84.3K
|
_transfer(from, to, value); |
|
| 146 |
return true; |
||
| 147 |
} |
||
| 148 | |||
| 149 |
/** |
||
| 150 |
* @dev Moves a `value` amount of tokens from `from` to `to`. |
||
| 151 |
* |
||
| 152 |
* This internal function is equivalent to {transfer}, and can be used to
|
||
| 153 |
* e.g. implement automatic token fees, slashing mechanisms, etc. |
||
| 154 |
* |
||
| 155 |
* Emits a {Transfer} event.
|
||
| 156 |
* |
||
| 157 |
* NOTE: This function is not virtual, {_update} should be overridden instead.
|
||
| 158 |
*/ |
||
| 159 |
function _transfer(address from, address to, uint256 value) internal {
|
||
| 160 |
✓ 154.9K
|
if (from == address(0)) {
|
|
| 161 |
revert ERC20InvalidSender(address(0)); |
||
| 162 |
} |
||
| 163 |
✓ 154.9K
|
if (to == address(0)) {
|
|
| 164 |
revert ERC20InvalidReceiver(address(0)); |
||
| 165 |
} |
||
| 166 |
_update(from, to, value); |
||
| 167 |
} |
||
| 168 | |||
| 169 |
/** |
||
| 170 |
* @dev Transfers a `value` amount of tokens from `from` to `to`, or alternatively mints (or burns) if `from` |
||
| 171 |
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding |
||
| 172 |
* this function. |
||
| 173 |
* |
||
| 174 |
* Emits a {Transfer} event.
|
||
| 175 |
*/ |
||
| 176 |
function _update(address from, address to, uint256 value) internal virtual {
|
||
| 177 |
if (from == address(0)) {
|
||
| 178 |
// Overflow check required: The rest of the code assumes that totalSupply never overflows |
||
| 179 |
✓ 113
|
_totalSupply += value; |
|
| 180 |
} else {
|
||
| 181 |
uint256 fromBalance = _balances[from]; |
||
| 182 |
✓ 154.9K
|
if (fromBalance < value) {
|
|
| 183 |
revert ERC20InsufficientBalance(from, fromBalance, value); |
||
| 184 |
} |
||
| 185 |
unchecked {
|
||
| 186 |
// Overflow not possible: value <= fromBalance <= totalSupply. |
||
| 187 |
_balances[from] = fromBalance - value; |
||
| 188 |
} |
||
| 189 |
} |
||
| 190 | |||
| 191 |
if (to == address(0)) {
|
||
| 192 |
unchecked {
|
||
| 193 |
// Overflow not possible: value <= totalSupply or value <= fromBalance <= totalSupply. |
||
| 194 |
_totalSupply -= value; |
||
| 195 |
} |
||
| 196 |
} else {
|
||
| 197 |
unchecked {
|
||
| 198 |
// Overflow not possible: balance + value is at most totalSupply, which we know fits into a uint256. |
||
| 199 |
_balances[to] += value; |
||
| 200 |
} |
||
| 201 |
} |
||
| 202 | |||
| 203 |
✓ 154.9K
|
emit Transfer(from, to, value); |
|
| 204 |
} |
||
| 205 | |||
| 206 |
/** |
||
| 207 |
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0). |
||
| 208 |
* Relies on the `_update` mechanism |
||
| 209 |
* |
||
| 210 |
* Emits a {Transfer} event with `from` set to the zero address.
|
||
| 211 |
* |
||
| 212 |
* NOTE: This function is not virtual, {_update} should be overridden instead.
|
||
| 213 |
*/ |
||
| 214 |
function _mint(address account, uint256 value) internal {
|
||
| 215 |
✓ 113
|
if (account == address(0)) {
|
|
| 216 |
revert ERC20InvalidReceiver(address(0)); |
||
| 217 |
} |
||
| 218 |
_update(address(0), account, value); |
||
| 219 |
} |
||
| 220 | |||
| 221 |
/** |
||
| 222 |
* @dev Destroys a `value` amount of tokens from `account`, lowering the total supply. |
||
| 223 |
* Relies on the `_update` mechanism. |
||
| 224 |
* |
||
| 225 |
* Emits a {Transfer} event with `to` set to the zero address.
|
||
| 226 |
* |
||
| 227 |
* NOTE: This function is not virtual, {_update} should be overridden instead
|
||
| 228 |
*/ |
||
| 229 |
function _burn(address account, uint256 value) internal {
|
||
| 230 |
if (account == address(0)) {
|
||
| 231 |
revert ERC20InvalidSender(address(0)); |
||
| 232 |
} |
||
| 233 |
_update(account, address(0), value); |
||
| 234 |
} |
||
| 235 | |||
| 236 |
/** |
||
| 237 |
* @dev Sets `value` as the allowance of `spender` over the `owner`'s tokens. |
||
| 238 |
* |
||
| 239 |
* This internal function is equivalent to `approve`, and can be used to |
||
| 240 |
* e.g. set automatic allowances for certain subsystems, etc. |
||
| 241 |
* |
||
| 242 |
* Emits an {Approval} event.
|
||
| 243 |
* |
||
| 244 |
* Requirements: |
||
| 245 |
* |
||
| 246 |
* - `owner` cannot be the zero address. |
||
| 247 |
* - `spender` cannot be the zero address. |
||
| 248 |
* |
||
| 249 |
* Overrides to this logic should be done to the variant with an additional `bool emitEvent` argument. |
||
| 250 |
*/ |
||
| 251 |
function _approve(address owner, address spender, uint256 value) internal {
|
||
| 252 |
_approve(owner, spender, value, true); |
||
| 253 |
} |
||
| 254 | |||
| 255 |
/** |
||
| 256 |
* @dev Variant of {_approve} with an optional flag to enable or disable the {Approval} event.
|
||
| 257 |
* |
||
| 258 |
* By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by
|
||
| 259 |
* `_spendAllowance` during the `transferFrom` operation sets the flag to false. This saves gas by not emitting any |
||
| 260 |
* `Approval` event during `transferFrom` operations. |
||
| 261 |
* |
||
| 262 |
* Anyone who wishes to continue emitting `Approval` events on the `transferFrom` operation can force the flag to |
||
| 263 |
* true using the following override: |
||
| 264 |
* |
||
| 265 |
* ```solidity |
||
| 266 |
* function _approve(address owner, address spender, uint256 value, bool) internal virtual override {
|
||
| 267 |
* super._approve(owner, spender, value, true); |
||
| 268 |
* } |
||
| 269 |
* ``` |
||
| 270 |
* |
||
| 271 |
* Requirements are the same as {_approve}.
|
||
| 272 |
*/ |
||
| 273 |
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual {
|
||
| 274 |
✓ 84.3K
|
if (owner == address(0)) {
|
|
| 275 |
revert ERC20InvalidApprover(address(0)); |
||
| 276 |
} |
||
| 277 |
✓ 84.3K
|
if (spender == address(0)) {
|
|
| 278 |
revert ERC20InvalidSpender(address(0)); |
||
| 279 |
} |
||
| 280 |
_allowances[owner][spender] = value; |
||
| 281 |
if (emitEvent) {
|
||
| 282 |
✓ 84.3K
|
emit Approval(owner, spender, value); |
|
| 283 |
} |
||
| 284 |
} |
||
| 285 | |||
| 286 |
/** |
||
| 287 |
* @dev Updates `owner`'s allowance for `spender` based on spent `value`. |
||
| 288 |
* |
||
| 289 |
* Does not update the allowance value in case of infinite allowance. |
||
| 290 |
* Revert if not enough allowance is available. |
||
| 291 |
* |
||
| 292 |
* Does not emit an {Approval} event.
|
||
| 293 |
*/ |
||
| 294 |
function _spendAllowance(address owner, address spender, uint256 value) internal virtual {
|
||
| 295 |
uint256 currentAllowance = allowance(owner, spender); |
||
| 296 |
✓ 84.3K
|
if (currentAllowance < type(uint256).max) {
|
|
| 297 |
✓ 84.3K
|
if (currentAllowance < value) {
|
|
| 298 |
revert ERC20InsufficientAllowance(spender, currentAllowance, value); |
||
| 299 |
} |
||
| 300 |
unchecked {
|
||
| 301 |
_approve(owner, spender, currentAllowance - value, false); |
||
| 302 |
} |
||
| 303 |
} |
||
| 304 |
} |
||
| 305 |
} |
||
| 306 |
0.0%
lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/IERC20.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.4.16; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev Interface of the ERC-20 standard as defined in the ERC. |
||
| 8 |
*/ |
||
| 9 |
interface IERC20 {
|
||
| 10 |
/** |
||
| 11 |
* @dev Emitted when `value` tokens are moved from one account (`from`) to |
||
| 12 |
* another (`to`). |
||
| 13 |
* |
||
| 14 |
* Note that `value` may be zero. |
||
| 15 |
*/ |
||
| 16 |
event Transfer(address indexed from, address indexed to, uint256 value); |
||
| 17 | |||
| 18 |
/** |
||
| 19 |
* @dev Emitted when the allowance of a `spender` for an `owner` is set by |
||
| 20 |
* a call to {approve}. `value` is the new allowance.
|
||
| 21 |
*/ |
||
| 22 |
event Approval(address indexed owner, address indexed spender, uint256 value); |
||
| 23 | |||
| 24 |
/** |
||
| 25 |
* @dev Returns the value of tokens in existence. |
||
| 26 |
*/ |
||
| 27 |
function totalSupply() external view returns (uint256); |
||
| 28 | |||
| 29 |
/** |
||
| 30 |
* @dev Returns the value of tokens owned by `account`. |
||
| 31 |
*/ |
||
| 32 |
function balanceOf(address account) external view returns (uint256); |
||
| 33 | |||
| 34 |
/** |
||
| 35 |
* @dev Moves a `value` amount of tokens from the caller's account to `to`. |
||
| 36 |
* |
||
| 37 |
* Returns a boolean value indicating whether the operation succeeded. |
||
| 38 |
* |
||
| 39 |
* Emits a {Transfer} event.
|
||
| 40 |
*/ |
||
| 41 |
function transfer(address to, uint256 value) external returns (bool); |
||
| 42 | |||
| 43 |
/** |
||
| 44 |
* @dev Returns the remaining number of tokens that `spender` will be |
||
| 45 |
* allowed to spend on behalf of `owner` through {transferFrom}. This is
|
||
| 46 |
* zero by default. |
||
| 47 |
* |
||
| 48 |
* This value changes when {approve} or {transferFrom} are called.
|
||
| 49 |
*/ |
||
| 50 |
function allowance(address owner, address spender) external view returns (uint256); |
||
| 51 | |||
| 52 |
/** |
||
| 53 |
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the |
||
| 54 |
* caller's tokens. |
||
| 55 |
* |
||
| 56 |
* Returns a boolean value indicating whether the operation succeeded. |
||
| 57 |
* |
||
| 58 |
* IMPORTANT: Beware that changing an allowance with this method brings the risk |
||
| 59 |
* that someone may use both the old and the new allowance by unfortunate |
||
| 60 |
* transaction ordering. One possible solution to mitigate this race |
||
| 61 |
* condition is to first reduce the spender's allowance to 0 and set the |
||
| 62 |
* desired value afterwards: |
||
| 63 |
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 |
||
| 64 |
* |
||
| 65 |
* Emits an {Approval} event.
|
||
| 66 |
*/ |
||
| 67 |
function approve(address spender, uint256 value) external returns (bool); |
||
| 68 | |||
| 69 |
/** |
||
| 70 |
* @dev Moves a `value` amount of tokens from `from` to `to` using the |
||
| 71 |
* allowance mechanism. `value` is then deducted from the caller's |
||
| 72 |
* allowance. |
||
| 73 |
* |
||
| 74 |
* Returns a boolean value indicating whether the operation succeeded. |
||
| 75 |
* |
||
| 76 |
* Emits a {Transfer} event.
|
||
| 77 |
*/ |
||
| 78 |
function transferFrom(address from, address to, uint256 value) external returns (bool); |
||
| 79 |
} |
||
| 80 |
0.0%
lib/openzeppelin-contracts/contracts/token/ERC20/extensions/IERC20Metadata.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/IERC20Metadata.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.6.2; |
||
| 5 | |||
| 6 |
import {IERC20} from "../IERC20.sol";
|
||
| 7 | |||
| 8 |
/** |
||
| 9 |
* @dev Interface for the optional metadata functions from the ERC-20 standard. |
||
| 10 |
*/ |
||
| 11 |
interface IERC20Metadata is IERC20 {
|
||
| 12 |
/** |
||
| 13 |
* @dev Returns the name of the token. |
||
| 14 |
*/ |
||
| 15 |
function name() external view returns (string memory); |
||
| 16 | |||
| 17 |
/** |
||
| 18 |
* @dev Returns the symbol of the token. |
||
| 19 |
*/ |
||
| 20 |
function symbol() external view returns (string memory); |
||
| 21 | |||
| 22 |
/** |
||
| 23 |
* @dev Returns the decimals places of the token. |
||
| 24 |
*/ |
||
| 25 |
function decimals() external view returns (uint8); |
||
| 26 |
} |
||
| 27 |
75.0%
lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol
Lines covered: 6 / 8 (75.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.5.0) (token/ERC20/utils/SafeERC20.sol) |
||
| 3 | |||
| 4 |
pragma solidity ^0.8.20; |
||
| 5 | |||
| 6 |
import {IERC20} from "../IERC20.sol";
|
||
| 7 |
import {IERC1363} from "../../../interfaces/IERC1363.sol";
|
||
| 8 | |||
| 9 |
/** |
||
| 10 |
* @title SafeERC20 |
||
| 11 |
* @dev Wrappers around ERC-20 operations that throw on failure (when the token |
||
| 12 |
* contract returns false). Tokens that return no value (and instead revert or |
||
| 13 |
* throw on failure) are also supported, non-reverting calls are assumed to be |
||
| 14 |
* successful. |
||
| 15 |
* To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract, |
||
| 16 |
* which allows you to call the safe operations as `token.safeTransfer(...)`, etc. |
||
| 17 |
*/ |
||
| 18 |
✓ 2
|
library SafeERC20 {
|
|
| 19 |
/** |
||
| 20 |
* @dev An operation with an ERC-20 token failed. |
||
| 21 |
*/ |
||
| 22 |
error SafeERC20FailedOperation(address token); |
||
| 23 | |||
| 24 |
/** |
||
| 25 |
* @dev Indicates a failed `decreaseAllowance` request. |
||
| 26 |
*/ |
||
| 27 |
error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); |
||
| 28 | |||
| 29 |
/** |
||
| 30 |
* @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, |
||
| 31 |
* non-reverting calls are assumed to be successful. |
||
| 32 |
*/ |
||
| 33 |
function safeTransfer(IERC20 token, address to, uint256 value) internal {
|
||
| 34 |
✓ 70.6K
|
if (!_safeTransfer(token, to, value, true)) {
|
|
| 35 |
revert SafeERC20FailedOperation(address(token)); |
||
| 36 |
} |
||
| 37 |
} |
||
| 38 | |||
| 39 |
/** |
||
| 40 |
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the |
||
| 41 |
* calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. |
||
| 42 |
*/ |
||
| 43 |
function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
|
||
| 44 |
✓ 67.1K
|
if (!_safeTransferFrom(token, from, to, value, true)) {
|
|
| 45 |
revert SafeERC20FailedOperation(address(token)); |
||
| 46 |
} |
||
| 47 |
} |
||
| 48 | |||
| 49 |
/** |
||
| 50 |
* @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful.
|
||
| 51 |
*/ |
||
| 52 |
function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) {
|
||
| 53 |
return _safeTransfer(token, to, value, false); |
||
| 54 |
} |
||
| 55 | |||
| 56 |
/** |
||
| 57 |
* @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful.
|
||
| 58 |
*/ |
||
| 59 |
function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) {
|
||
| 60 |
return _safeTransferFrom(token, from, to, value, false); |
||
| 61 |
} |
||
| 62 | |||
| 63 |
/** |
||
| 64 |
* @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, |
||
| 65 |
* non-reverting calls are assumed to be successful. |
||
| 66 |
* |
||
| 67 |
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client" |
||
| 68 |
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using |
||
| 69 |
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
|
||
| 70 |
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior. |
||
| 71 |
*/ |
||
| 72 |
function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
|
||
| 73 |
uint256 oldAllowance = token.allowance(address(this), spender); |
||
| 74 |
forceApprove(token, spender, oldAllowance + value); |
||
| 75 |
} |
||
| 76 | |||
| 77 |
/** |
||
| 78 |
* @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no |
||
| 79 |
* value, non-reverting calls are assumed to be successful. |
||
| 80 |
* |
||
| 81 |
* IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client" |
||
| 82 |
* smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using |
||
| 83 |
* this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract
|
||
| 84 |
* that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior. |
||
| 85 |
*/ |
||
| 86 |
function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal {
|
||
| 87 |
unchecked {
|
||
| 88 |
uint256 currentAllowance = token.allowance(address(this), spender); |
||
| 89 |
if (currentAllowance < requestedDecrease) {
|
||
| 90 |
revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); |
||
| 91 |
} |
||
| 92 |
forceApprove(token, spender, currentAllowance - requestedDecrease); |
||
| 93 |
} |
||
| 94 |
} |
||
| 95 | |||
| 96 |
/** |
||
| 97 |
* @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, |
||
| 98 |
* non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval |
||
| 99 |
* to be set to zero before setting it to a non-zero value, such as USDT. |
||
| 100 |
* |
||
| 101 |
* NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function |
||
| 102 |
* only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being |
||
| 103 |
* set here. |
||
| 104 |
*/ |
||
| 105 |
function forceApprove(IERC20 token, address spender, uint256 value) internal {
|
||
| 106 |
if (!_safeApprove(token, spender, value, false)) {
|
||
| 107 |
if (!_safeApprove(token, spender, 0, true)) revert SafeERC20FailedOperation(address(token)); |
||
| 108 |
if (!_safeApprove(token, spender, value, true)) revert SafeERC20FailedOperation(address(token)); |
||
| 109 |
} |
||
| 110 |
} |
||
| 111 | |||
| 112 |
/** |
||
| 113 |
* @dev Performs an {ERC1363} transferAndCall, with a fallback to the simple {ERC20} transfer if the target has no
|
||
| 114 |
* code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when
|
||
| 115 |
* targeting contracts. |
||
| 116 |
* |
||
| 117 |
* Reverts if the returned value is other than `true`. |
||
| 118 |
*/ |
||
| 119 |
function transferAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
|
||
| 120 |
if (to.code.length == 0) {
|
||
| 121 |
safeTransfer(token, to, value); |
||
| 122 |
} else if (!token.transferAndCall(to, value, data)) {
|
||
| 123 |
revert SafeERC20FailedOperation(address(token)); |
||
| 124 |
} |
||
| 125 |
} |
||
| 126 | |||
| 127 |
/** |
||
| 128 |
* @dev Performs an {ERC1363} transferFromAndCall, with a fallback to the simple {ERC20} transferFrom if the target
|
||
| 129 |
* has no code. This can be used to implement an {ERC721}-like safe transfer that relies on {ERC1363} checks when
|
||
| 130 |
* targeting contracts. |
||
| 131 |
* |
||
| 132 |
* Reverts if the returned value is other than `true`. |
||
| 133 |
*/ |
||
| 134 |
function transferFromAndCallRelaxed( |
||
| 135 |
IERC1363 token, |
||
| 136 |
address from, |
||
| 137 |
address to, |
||
| 138 |
uint256 value, |
||
| 139 |
bytes memory data |
||
| 140 |
) internal {
|
||
| 141 |
if (to.code.length == 0) {
|
||
| 142 |
safeTransferFrom(token, from, to, value); |
||
| 143 |
} else if (!token.transferFromAndCall(from, to, value, data)) {
|
||
| 144 |
revert SafeERC20FailedOperation(address(token)); |
||
| 145 |
} |
||
| 146 |
} |
||
| 147 | |||
| 148 |
/** |
||
| 149 |
* @dev Performs an {ERC1363} approveAndCall, with a fallback to the simple {ERC20} approve if the target has no
|
||
| 150 |
* code. This can be used to implement an {ERC721}-like safe transfer that rely on {ERC1363} checks when
|
||
| 151 |
* targeting contracts. |
||
| 152 |
* |
||
| 153 |
* NOTE: When the recipient address (`to`) has no code (i.e. is an EOA), this function behaves as {forceApprove}.
|
||
| 154 |
* Oppositely, when the recipient address (`to`) has code, this function only attempts to call {ERC1363-approveAndCall}
|
||
| 155 |
* once without retrying, and relies on the returned value to be true. |
||
| 156 |
* |
||
| 157 |
* Reverts if the returned value is other than `true`. |
||
| 158 |
*/ |
||
| 159 |
function approveAndCallRelaxed(IERC1363 token, address to, uint256 value, bytes memory data) internal {
|
||
| 160 |
if (to.code.length == 0) {
|
||
| 161 |
forceApprove(token, to, value); |
||
| 162 |
} else if (!token.approveAndCall(to, value, data)) {
|
||
| 163 |
revert SafeERC20FailedOperation(address(token)); |
||
| 164 |
} |
||
| 165 |
} |
||
| 166 | |||
| 167 |
/** |
||
| 168 |
* @dev Imitates a Solidity `token.transfer(to, value)` call, relaxing the requirement on the return value: the |
||
| 169 |
* return value is optional (but if data is returned, it must not be false). |
||
| 170 |
* |
||
| 171 |
* @param token The token targeted by the call. |
||
| 172 |
* @param to The recipient of the tokens |
||
| 173 |
* @param value The amount of token to transfer |
||
| 174 |
* @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean. |
||
| 175 |
*/ |
||
| 176 |
function _safeTransfer(IERC20 token, address to, uint256 value, bool bubble) private returns (bool success) {
|
||
| 177 |
✓ 70.6K
|
bytes4 selector = IERC20.transfer.selector; |
|
| 178 | |||
| 179 |
✓ 70.6K
|
assembly ("memory-safe") {
|
|
| 180 |
let fmp := mload(0x40) |
||
| 181 |
mstore(0x00, selector) |
||
| 182 |
mstore(0x04, and(to, shr(96, not(0)))) |
||
| 183 |
mstore(0x24, value) |
||
| 184 |
success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20) |
||
| 185 |
// if call success and return is true, all is good. |
||
| 186 |
// otherwise (not success or return is not true), we need to perform further checks |
||
| 187 |
if iszero(and(success, eq(mload(0x00), 1))) {
|
||
| 188 |
// if the call was a failure and bubble is enabled, bubble the error |
||
| 189 |
if and(iszero(success), bubble) {
|
||
| 190 |
returndatacopy(fmp, 0x00, returndatasize()) |
||
| 191 |
revert(fmp, returndatasize()) |
||
| 192 |
} |
||
| 193 |
// if the return value is not true, then the call is only successful if: |
||
| 194 |
// - the token address has code |
||
| 195 |
// - the returndata is empty |
||
| 196 |
success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0))) |
||
| 197 |
} |
||
| 198 |
mstore(0x40, fmp) |
||
| 199 |
} |
||
| 200 |
} |
||
| 201 | |||
| 202 |
/** |
||
| 203 |
* @dev Imitates a Solidity `token.transferFrom(from, to, value)` call, relaxing the requirement on the return |
||
| 204 |
* value: the return value is optional (but if data is returned, it must not be false). |
||
| 205 |
* |
||
| 206 |
* @param token The token targeted by the call. |
||
| 207 |
* @param from The sender of the tokens |
||
| 208 |
* @param to The recipient of the tokens |
||
| 209 |
* @param value The amount of token to transfer |
||
| 210 |
* @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean. |
||
| 211 |
*/ |
||
| 212 |
function _safeTransferFrom( |
||
| 213 |
IERC20 token, |
||
| 214 |
address from, |
||
| 215 |
address to, |
||
| 216 |
uint256 value, |
||
| 217 |
bool bubble |
||
| 218 |
) private returns (bool success) {
|
||
| 219 |
bytes4 selector = IERC20.transferFrom.selector; |
||
| 220 | |||
| 221 |
✓ 67.1K
|
assembly ("memory-safe") {
|
|
| 222 |
let fmp := mload(0x40) |
||
| 223 |
mstore(0x00, selector) |
||
| 224 |
mstore(0x04, and(from, shr(96, not(0)))) |
||
| 225 |
mstore(0x24, and(to, shr(96, not(0)))) |
||
| 226 |
mstore(0x44, value) |
||
| 227 |
success := call(gas(), token, 0, 0x00, 0x64, 0x00, 0x20) |
||
| 228 |
// if call success and return is true, all is good. |
||
| 229 |
// otherwise (not success or return is not true), we need to perform further checks |
||
| 230 |
if iszero(and(success, eq(mload(0x00), 1))) {
|
||
| 231 |
// if the call was a failure and bubble is enabled, bubble the error |
||
| 232 |
if and(iszero(success), bubble) {
|
||
| 233 |
returndatacopy(fmp, 0x00, returndatasize()) |
||
| 234 |
revert(fmp, returndatasize()) |
||
| 235 |
} |
||
| 236 |
// if the return value is not true, then the call is only successful if: |
||
| 237 |
// - the token address has code |
||
| 238 |
// - the returndata is empty |
||
| 239 |
success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0))) |
||
| 240 |
} |
||
| 241 |
mstore(0x40, fmp) |
||
| 242 |
mstore(0x60, 0) |
||
| 243 |
} |
||
| 244 |
} |
||
| 245 | |||
| 246 |
/** |
||
| 247 |
* @dev Imitates a Solidity `token.approve(spender, value)` call, relaxing the requirement on the return value: |
||
| 248 |
* the return value is optional (but if data is returned, it must not be false). |
||
| 249 |
* |
||
| 250 |
* @param token The token targeted by the call. |
||
| 251 |
* @param spender The spender of the tokens |
||
| 252 |
* @param value The amount of token to transfer |
||
| 253 |
* @param bubble Behavior switch if the transfer call reverts: bubble the revert reason or return a false boolean. |
||
| 254 |
*/ |
||
| 255 |
function _safeApprove(IERC20 token, address spender, uint256 value, bool bubble) private returns (bool success) {
|
||
| 256 |
bytes4 selector = IERC20.approve.selector; |
||
| 257 | |||
| 258 |
assembly ("memory-safe") {
|
||
| 259 |
let fmp := mload(0x40) |
||
| 260 |
mstore(0x00, selector) |
||
| 261 |
mstore(0x04, and(spender, shr(96, not(0)))) |
||
| 262 |
mstore(0x24, value) |
||
| 263 |
success := call(gas(), token, 0, 0x00, 0x44, 0x00, 0x20) |
||
| 264 |
// if call success and return is true, all is good. |
||
| 265 |
// otherwise (not success or return is not true), we need to perform further checks |
||
| 266 |
if iszero(and(success, eq(mload(0x00), 1))) {
|
||
| 267 |
// if the call was a failure and bubble is enabled, bubble the error |
||
| 268 |
if and(iszero(success), bubble) {
|
||
| 269 |
returndatacopy(fmp, 0x00, returndatasize()) |
||
| 270 |
revert(fmp, returndatasize()) |
||
| 271 |
} |
||
| 272 |
// if the return value is not true, then the call is only successful if: |
||
| 273 |
// - the token address has code |
||
| 274 |
// - the returndata is empty |
||
| 275 |
success := and(success, and(iszero(returndatasize()), gt(extcodesize(token), 0))) |
||
| 276 |
} |
||
| 277 |
mstore(0x40, fmp) |
||
| 278 |
} |
||
| 279 |
} |
||
| 280 |
} |
||
| 281 |
100.0%
lib/openzeppelin-contracts/contracts/utils/Context.sol
Lines covered: 1 / 1 (100.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.0.1) (utils/Context.sol) |
||
| 3 | |||
| 4 |
pragma solidity ^0.8.20; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev Provides information about the current execution context, including the |
||
| 8 |
* sender of the transaction and its data. While these are generally available |
||
| 9 |
* via msg.sender and msg.data, they should not be accessed in such a direct |
||
| 10 |
* manner, since when dealing with meta-transactions the account sending and |
||
| 11 |
* paying for execution may not be the actual sender (as far as an application |
||
| 12 |
* is concerned). |
||
| 13 |
* |
||
| 14 |
* This contract is only required for intermediate, library-like contracts. |
||
| 15 |
*/ |
||
| 16 |
abstract contract Context {
|
||
| 17 |
function _msgSender() internal view virtual returns (address) {
|
||
| 18 |
✓ 135.5K
|
return msg.sender; |
|
| 19 |
} |
||
| 20 | |||
| 21 |
function _msgData() internal view virtual returns (bytes calldata) {
|
||
| 22 |
return msg.data; |
||
| 23 |
} |
||
| 24 | |||
| 25 |
function _contextSuffixLength() internal view virtual returns (uint256) {
|
||
| 26 |
return 0; |
||
| 27 |
} |
||
| 28 |
} |
||
| 29 |
83.3%
lib/openzeppelin-contracts/contracts/utils/ReentrancyGuard.sol
Lines covered: 5 / 6 (83.3%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.5.0) (utils/ReentrancyGuard.sol) |
||
| 3 | |||
| 4 |
pragma solidity ^0.8.20; |
||
| 5 | |||
| 6 |
import {StorageSlot} from "./StorageSlot.sol";
|
||
| 7 | |||
| 8 |
/** |
||
| 9 |
* @dev Contract module that helps prevent reentrant calls to a function. |
||
| 10 |
* |
||
| 11 |
* Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
|
||
| 12 |
* available, which can be applied to functions to make sure there are no nested |
||
| 13 |
* (reentrant) calls to them. |
||
| 14 |
* |
||
| 15 |
* Note that because there is a single `nonReentrant` guard, functions marked as |
||
| 16 |
* `nonReentrant` may not call one another. This can be worked around by making |
||
| 17 |
* those functions `private`, and then adding `external` `nonReentrant` entry |
||
| 18 |
* points to them. |
||
| 19 |
* |
||
| 20 |
* TIP: If EIP-1153 (transient storage) is available on the chain you're deploying at, |
||
| 21 |
* consider using {ReentrancyGuardTransient} instead.
|
||
| 22 |
* |
||
| 23 |
* TIP: If you would like to learn more about reentrancy and alternative ways |
||
| 24 |
* to protect against it, check out our blog post |
||
| 25 |
* https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. |
||
| 26 |
* |
||
| 27 |
* IMPORTANT: Deprecated. This storage-based reentrancy guard will be removed and replaced |
||
| 28 |
* by the {ReentrancyGuardTransient} variant in v6.0.
|
||
| 29 |
* |
||
| 30 |
* @custom:stateless |
||
| 31 |
*/ |
||
| 32 |
abstract contract ReentrancyGuard {
|
||
| 33 |
using StorageSlot for bytes32; |
||
| 34 | |||
| 35 |
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff))
|
||
| 36 |
bytes32 private constant REENTRANCY_GUARD_STORAGE = |
||
| 37 |
✓ 1
|
0x9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00; |
|
| 38 | |||
| 39 |
// Booleans are more expensive than uint256 or any type that takes up a full |
||
| 40 |
// word because each write operation emits an extra SLOAD to first read the |
||
| 41 |
// slot's contents, replace the bits taken up by the boolean, and then write |
||
| 42 |
// back. This is the compiler's defense against contract upgrades and |
||
| 43 |
// pointer aliasing, and it cannot be disabled. |
||
| 44 | |||
| 45 |
// The values being non-zero value makes deployment a bit more expensive, |
||
| 46 |
// but in exchange the refund on every call to nonReentrant will be lower in |
||
| 47 |
// amount. Since refunds are capped to a percentage of the total |
||
| 48 |
// transaction's gas, it is best to keep them low in cases like this one, to |
||
| 49 |
// increase the likelihood of the full refund coming into effect. |
||
| 50 |
uint256 private constant NOT_ENTERED = 1; |
||
| 51 |
✓ 52.5K
|
uint256 private constant ENTERED = 2; |
|
| 52 | |||
| 53 |
/** |
||
| 54 |
* @dev Unauthorized reentrant call. |
||
| 55 |
*/ |
||
| 56 |
error ReentrancyGuardReentrantCall(); |
||
| 57 | |||
| 58 |
constructor() {
|
||
| 59 |
_reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED; |
||
| 60 |
} |
||
| 61 | |||
| 62 |
/** |
||
| 63 |
* @dev Prevents a contract from calling itself, directly or indirectly. |
||
| 64 |
* Calling a `nonReentrant` function from another `nonReentrant` |
||
| 65 |
* function is not supported. It is possible to prevent this from happening |
||
| 66 |
* by making the `nonReentrant` function external, and making it call a |
||
| 67 |
* `private` function that does the actual work. |
||
| 68 |
*/ |
||
| 69 |
✓ 35.3K
|
modifier nonReentrant() {
|
|
| 70 |
_nonReentrantBefore(); |
||
| 71 |
_; |
||
| 72 |
_nonReentrantAfter(); |
||
| 73 |
} |
||
| 74 | |||
| 75 |
/** |
||
| 76 |
* @dev A `view` only version of {nonReentrant}. Use to block view functions
|
||
| 77 |
* from being called, preventing reading from inconsistent contract state. |
||
| 78 |
* |
||
| 79 |
* CAUTION: This is a "view" modifier and does not change the reentrancy |
||
| 80 |
* status. Use it only on view functions. For payable or non-payable functions, |
||
| 81 |
* use the standard {nonReentrant} modifier instead.
|
||
| 82 |
*/ |
||
| 83 |
modifier nonReentrantView() {
|
||
| 84 |
_nonReentrantBeforeView(); |
||
| 85 |
_; |
||
| 86 |
} |
||
| 87 | |||
| 88 |
function _nonReentrantBeforeView() private view {
|
||
| 89 |
if (_reentrancyGuardEntered()) {
|
||
| 90 |
revert ReentrancyGuardReentrantCall(); |
||
| 91 |
} |
||
| 92 |
} |
||
| 93 | |||
| 94 |
✓ 52.5K
|
function _nonReentrantBefore() private {
|
|
| 95 |
// On the first call to nonReentrant, _status will be NOT_ENTERED |
||
| 96 |
_nonReentrantBeforeView(); |
||
| 97 | |||
| 98 |
// Any calls to nonReentrant after this point will fail |
||
| 99 |
_reentrancyGuardStorageSlot().getUint256Slot().value = ENTERED; |
||
| 100 |
} |
||
| 101 | |||
| 102 |
function _nonReentrantAfter() private {
|
||
| 103 |
// By storing the original value once again, a refund is triggered (see |
||
| 104 |
// https://eips.ethereum.org/EIPS/eip-2200) |
||
| 105 |
_reentrancyGuardStorageSlot().getUint256Slot().value = NOT_ENTERED; |
||
| 106 |
} |
||
| 107 | |||
| 108 |
/** |
||
| 109 |
* @dev Returns true if the reentrancy guard is currently set to "entered", which indicates there is a |
||
| 110 |
* `nonReentrant` function in the call stack. |
||
| 111 |
*/ |
||
| 112 |
function _reentrancyGuardEntered() internal view returns (bool) {
|
||
| 113 |
✓ 52.5K
|
return _reentrancyGuardStorageSlot().getUint256Slot().value == ENTERED; |
|
| 114 |
} |
||
| 115 | |||
| 116 |
function _reentrancyGuardStorageSlot() internal pure virtual returns (bytes32) {
|
||
| 117 |
return REENTRANCY_GUARD_STORAGE; |
||
| 118 |
} |
||
| 119 |
} |
||
| 120 |
100.0%
lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol
Lines covered: 1 / 1 (100.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol) |
||
| 3 |
// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. |
||
| 4 | |||
| 5 |
pragma solidity ^0.8.20; |
||
| 6 | |||
| 7 |
/** |
||
| 8 |
* @dev Library for reading and writing primitive types to specific storage slots. |
||
| 9 |
* |
||
| 10 |
* Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. |
||
| 11 |
* This library helps with reading and writing to such slots without the need for inline assembly. |
||
| 12 |
* |
||
| 13 |
* The functions in this library return Slot structs that contain a `value` member that can be used to read or write. |
||
| 14 |
* |
||
| 15 |
* Example usage to set ERC-1967 implementation slot: |
||
| 16 |
* ```solidity |
||
| 17 |
* contract ERC1967 {
|
||
| 18 |
* // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. |
||
| 19 |
* bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; |
||
| 20 |
* |
||
| 21 |
* function _getImplementation() internal view returns (address) {
|
||
| 22 |
* return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; |
||
| 23 |
* } |
||
| 24 |
* |
||
| 25 |
* function _setImplementation(address newImplementation) internal {
|
||
| 26 |
* require(newImplementation.code.length > 0); |
||
| 27 |
* StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; |
||
| 28 |
* } |
||
| 29 |
* } |
||
| 30 |
* ``` |
||
| 31 |
* |
||
| 32 |
* TIP: Consider using this library along with {SlotDerivation}.
|
||
| 33 |
*/ |
||
| 34 |
✓ 2
|
library StorageSlot {
|
|
| 35 |
struct AddressSlot {
|
||
| 36 |
address value; |
||
| 37 |
} |
||
| 38 | |||
| 39 |
struct BooleanSlot {
|
||
| 40 |
bool value; |
||
| 41 |
} |
||
| 42 | |||
| 43 |
struct Bytes32Slot {
|
||
| 44 |
bytes32 value; |
||
| 45 |
} |
||
| 46 | |||
| 47 |
struct Uint256Slot {
|
||
| 48 |
uint256 value; |
||
| 49 |
} |
||
| 50 | |||
| 51 |
struct Int256Slot {
|
||
| 52 |
int256 value; |
||
| 53 |
} |
||
| 54 | |||
| 55 |
struct StringSlot {
|
||
| 56 |
string value; |
||
| 57 |
} |
||
| 58 | |||
| 59 |
struct BytesSlot {
|
||
| 60 |
bytes value; |
||
| 61 |
} |
||
| 62 | |||
| 63 |
/** |
||
| 64 |
* @dev Returns an `AddressSlot` with member `value` located at `slot`. |
||
| 65 |
*/ |
||
| 66 |
function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
|
||
| 67 |
assembly ("memory-safe") {
|
||
| 68 |
r.slot := slot |
||
| 69 |
} |
||
| 70 |
} |
||
| 71 | |||
| 72 |
/** |
||
| 73 |
* @dev Returns a `BooleanSlot` with member `value` located at `slot`. |
||
| 74 |
*/ |
||
| 75 |
function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
|
||
| 76 |
assembly ("memory-safe") {
|
||
| 77 |
r.slot := slot |
||
| 78 |
} |
||
| 79 |
} |
||
| 80 | |||
| 81 |
/** |
||
| 82 |
* @dev Returns a `Bytes32Slot` with member `value` located at `slot`. |
||
| 83 |
*/ |
||
| 84 |
function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
|
||
| 85 |
assembly ("memory-safe") {
|
||
| 86 |
r.slot := slot |
||
| 87 |
} |
||
| 88 |
} |
||
| 89 | |||
| 90 |
/** |
||
| 91 |
* @dev Returns a `Uint256Slot` with member `value` located at `slot`. |
||
| 92 |
*/ |
||
| 93 |
function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
|
||
| 94 |
assembly ("memory-safe") {
|
||
| 95 |
r.slot := slot |
||
| 96 |
} |
||
| 97 |
} |
||
| 98 | |||
| 99 |
/** |
||
| 100 |
* @dev Returns a `Int256Slot` with member `value` located at `slot`. |
||
| 101 |
*/ |
||
| 102 |
function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
|
||
| 103 |
assembly ("memory-safe") {
|
||
| 104 |
r.slot := slot |
||
| 105 |
} |
||
| 106 |
} |
||
| 107 | |||
| 108 |
/** |
||
| 109 |
* @dev Returns a `StringSlot` with member `value` located at `slot`. |
||
| 110 |
*/ |
||
| 111 |
function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
|
||
| 112 |
assembly ("memory-safe") {
|
||
| 113 |
r.slot := slot |
||
| 114 |
} |
||
| 115 |
} |
||
| 116 | |||
| 117 |
/** |
||
| 118 |
* @dev Returns an `StringSlot` representation of the string storage pointer `store`. |
||
| 119 |
*/ |
||
| 120 |
function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
|
||
| 121 |
assembly ("memory-safe") {
|
||
| 122 |
r.slot := store.slot |
||
| 123 |
} |
||
| 124 |
} |
||
| 125 | |||
| 126 |
/** |
||
| 127 |
* @dev Returns a `BytesSlot` with member `value` located at `slot`. |
||
| 128 |
*/ |
||
| 129 |
function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
|
||
| 130 |
assembly ("memory-safe") {
|
||
| 131 |
r.slot := slot |
||
| 132 |
} |
||
| 133 |
} |
||
| 134 | |||
| 135 |
/** |
||
| 136 |
* @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. |
||
| 137 |
*/ |
||
| 138 |
function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
|
||
| 139 |
assembly ("memory-safe") {
|
||
| 140 |
r.slot := store.slot |
||
| 141 |
} |
||
| 142 |
} |
||
| 143 |
} |
||
| 144 |
0.0%
lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
// OpenZeppelin Contracts (last updated v5.4.0) (utils/introspection/IERC165.sol) |
||
| 3 | |||
| 4 |
pragma solidity >=0.4.16; |
||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @dev Interface of the ERC-165 standard, as defined in the |
||
| 8 |
* https://eips.ethereum.org/EIPS/eip-165[ERC]. |
||
| 9 |
* |
||
| 10 |
* Implementers can declare support of contract interfaces, which can then be |
||
| 11 |
* queried by others ({ERC165Checker}).
|
||
| 12 |
* |
||
| 13 |
* For an implementation, see {ERC165}.
|
||
| 14 |
*/ |
||
| 15 |
interface IERC165 {
|
||
| 16 |
/** |
||
| 17 |
* @dev Returns true if this contract implements the interface defined by |
||
| 18 |
* `interfaceId`. See the corresponding |
||
| 19 |
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[ERC section] |
||
| 20 |
* to learn more about how these ids are created. |
||
| 21 |
* |
||
| 22 |
* This function call must use less than 30 000 gas. |
||
| 23 |
*/ |
||
| 24 |
function supportsInterface(bytes4 interfaceId) external view returns (bool); |
||
| 25 |
} |
||
| 26 |
58.2%
src/CreditPolicy.sol
Lines covered: 57 / 98 (58.2%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 |
import {ICreditPolicy} from "./interfaces/ICreditPolicy.sol";
|
||
| 4 | |||
| 5 | |||
| 6 |
/** |
||
| 7 |
* @title CreditPolicy |
||
| 8 |
* @notice Immutable-by-version credit constitution for private credit funds |
||
| 9 |
*/ |
||
| 10 |
contract CreditPolicy is ICreditPolicy {
|
||
| 11 |
/*////////////////////////////////////////////////////////////// |
||
| 12 |
ERRORS |
||
| 13 |
//////////////////////////////////////////////////////////////*/ |
||
| 14 |
error CreditPolicy__Unauthorized(); |
||
| 15 |
error CreditPolicy__PolicyFrozen(uint256 version); |
||
| 16 |
error CreditPolicy__InvalidVersion(); |
||
| 17 |
error CreditPolicy__PolicyVersionExists(uint256 version); |
||
| 18 |
error CreditPolicy__InvalidAdmin(); |
||
| 19 |
error CreditPolicy__PolicyNotEditable(uint256 version); |
||
| 20 |
error CreditPolicy__IncompletePolicy(uint256 version); |
||
| 21 |
error CreditPolicy__InvalidIndustryHash(); |
||
| 22 |
error CreditPolicy__PolicyNotActive(uint256 version); |
||
| 23 |
error CreditPolicy__InvalidTierCount(uint256 count); |
||
| 24 |
/*////////////////////////////////////////////////////////////// |
||
| 25 |
MODIFIERS |
||
| 26 |
//////////////////////////////////////////////////////////////*/ |
||
| 27 |
✓ 1
|
modifier onlyAdmin() {
|
|
| 28 |
_onlyAdmin(); |
||
| 29 |
_; |
||
| 30 |
} |
||
| 31 | |||
| 32 |
function _onlyAdmin() internal view {
|
||
| 33 |
✓ 10
|
if (msg.sender != policyAdmin) revert CreditPolicy__Unauthorized(); |
|
| 34 |
} |
||
| 35 | |||
| 36 |
modifier policyEditable(uint256 version) {
|
||
| 37 |
✓ 1
|
_policyEditable(version); |
|
| 38 |
_; |
||
| 39 |
} |
||
| 40 | |||
| 41 |
function _policyEditable(uint256 version) internal view {
|
||
| 42 |
✓ 7
|
if (policyFrozen[version] || !policyActive[version]) |
|
| 43 |
revert CreditPolicy__PolicyNotEditable(version); |
||
| 44 |
} |
||
| 45 | |||
| 46 |
modifier policyExists(uint256 version) {
|
||
| 47 |
✓ 1
|
_policyExists(version); |
|
| 48 |
_; |
||
| 49 |
} |
||
| 50 | |||
| 51 |
function _policyExists(uint256 version) internal view {
|
||
| 52 |
✓ 8
|
if (!policyCreated[version]) revert CreditPolicy__InvalidVersion(); |
|
| 53 |
} |
||
| 54 | |||
| 55 |
/*////////////////////////////////////////////////////////////// |
||
| 56 |
CORE ROLES |
||
| 57 |
//////////////////////////////////////////////////////////////*/ |
||
| 58 |
address public policyAdmin; |
||
| 59 |
uint8 internal maxTiers; |
||
| 60 | |||
| 61 |
/*////////////////////////////////////////////////////////////// |
||
| 62 |
POLICY LIFECYCLE |
||
| 63 |
//////////////////////////////////////////////////////////////*/ |
||
| 64 |
mapping(uint256 => bool) public policyCreated; |
||
| 65 |
✓ 165.9K
|
mapping(uint256 => bool) public policyFrozen; |
|
| 66 |
mapping(uint256 => bool) public policyActive; |
||
| 67 |
mapping(uint256 => uint256) public lastUpdated; |
||
| 68 | |||
| 69 |
/*////////////////////////////////////////////////////////////// |
||
| 70 |
ELIGIBILITY (PRE-LOAN) |
||
| 71 |
//////////////////////////////////////////////////////////////*/ |
||
| 72 |
struct EligibilityCriteria {
|
||
| 73 |
uint256 minAnnualRevenue; |
||
| 74 |
uint256 minEBITDA; |
||
| 75 |
uint256 minTangibleNetWorth; |
||
| 76 |
uint256 minBusinessAgeDays; |
||
| 77 |
uint256 maxDefaultsLast36Months; |
||
| 78 |
bool bankruptcyExcluded; |
||
| 79 |
} |
||
| 80 | |||
| 81 |
mapping(uint256 => EligibilityCriteria) public eligibility; |
||
| 82 | |||
| 83 |
/*////////////////////////////////////////////////////////////// |
||
| 84 |
FINANCIAL RATIOS (UNDERWRITING) |
||
| 85 |
//////////////////////////////////////////////////////////////*/ |
||
| 86 |
struct FinancialRatios {
|
||
| 87 |
uint256 maxTotalDebtToEBITDA; |
||
| 88 |
uint256 minInterestCoverageRatio; |
||
| 89 |
uint256 minCurrentRatio; |
||
| 90 |
uint256 minEBITDAMarginBps; |
||
| 91 |
} |
||
| 92 | |||
| 93 |
mapping(uint256 => FinancialRatios) public ratios; |
||
| 94 | |||
| 95 |
/*////////////////////////////////////////////////////////////// |
||
| 96 |
LOAN TIERS (PRICING REFERENCE) |
||
| 97 |
//////////////////////////////////////////////////////////////*/ |
||
| 98 |
struct LoanTier {
|
||
| 99 |
string name; |
||
| 100 |
uint256 minRevenue; |
||
| 101 |
uint256 maxRevenue; |
||
| 102 |
uint256 minEBITDA; |
||
| 103 |
uint256 maxDebtToEBITDA; |
||
| 104 |
uint256 maxLoanToEBITDA; |
||
| 105 |
uint256 interestRateBps; |
||
| 106 |
uint256 originationFeeBps; |
||
| 107 |
uint256 termDays; |
||
| 108 |
bool active; |
||
| 109 |
} |
||
| 110 | |||
| 111 |
mapping(uint256 => mapping(uint8 => LoanTier)) public loanTiers; |
||
| 112 |
mapping(uint256 => uint8) public totalTiers; |
||
| 113 |
✓ 82.9K
|
mapping(uint256 => mapping(uint8 => bool)) public tierExists; |
|
| 114 | |||
| 115 |
/*////////////////////////////////////////////////////////////// |
||
| 116 |
CONCENTRATION LIMITS |
||
| 117 |
//////////////////////////////////////////////////////////////*/ |
||
| 118 |
struct ConcentrationLimits {
|
||
| 119 |
uint256 maxSingleBorrowerBps; |
||
| 120 |
uint256 maxIndustryConcentrationBps; |
||
| 121 |
} |
||
| 122 | |||
| 123 |
mapping(uint256 => ConcentrationLimits) public concentration; |
||
| 124 | |||
| 125 |
/*////////////////////////////////////////////////////////////// |
||
| 126 |
INDUSTRY EXCLUSIONS |
||
| 127 |
//////////////////////////////////////////////////////////////*/ |
||
| 128 |
mapping(uint256 => mapping(bytes32 => bool)) public excludedIndustries; |
||
| 129 | |||
| 130 |
/*////////////////////////////////////////////////////////////// |
||
| 131 |
ATTESTATION REQUIREMENTS |
||
| 132 |
//////////////////////////////////////////////////////////////*/ |
||
| 133 |
struct AttestationRequirements {
|
||
| 134 |
uint256 maxAttestationAgeDays; |
||
| 135 |
uint256 reAttestationFrequencyDays; |
||
| 136 |
bool requiresCPAAttestation; |
||
| 137 |
} |
||
| 138 | |||
| 139 |
mapping(uint256 => AttestationRequirements) public attestation; |
||
| 140 | |||
| 141 |
/*////////////////////////////////////////////////////////////// |
||
| 142 |
MAINTENANCE COVENANTS |
||
| 143 |
//////////////////////////////////////////////////////////////*/ |
||
| 144 |
struct MaintenanceCovenants {
|
||
| 145 |
uint256 maxLeverageRatio; |
||
| 146 |
uint256 minCoverageRatio; |
||
| 147 |
uint256 minLiquidityAmount; |
||
| 148 |
bool allowsDividends; |
||
| 149 |
uint256 reportingFrequencyDays; |
||
| 150 |
} |
||
| 151 | |||
| 152 |
mapping(uint256 => MaintenanceCovenants) public covenants; |
||
| 153 | |||
| 154 |
/*////////////////////////////////////////////////////////////// |
||
| 155 |
DOCUMENT ANCHORING |
||
| 156 |
//////////////////////////////////////////////////////////////*/ |
||
| 157 |
mapping(uint256 => bytes32) public policyDocumentHash; |
||
| 158 |
mapping(uint256 => string) public policyDocumentURI; |
||
| 159 | |||
| 160 |
mapping(uint256 => bool) public eligibilitySet; |
||
| 161 |
mapping(uint256 => bool) public ratiosSet; |
||
| 162 |
mapping(uint256 => bool) public concentrationSet; |
||
| 163 |
mapping(uint256 => bool) public attestationSet; |
||
| 164 |
mapping(uint256 => bool) public covenantsSet; |
||
| 165 |
mapping(uint256 => bool) public hasAtLeastOneTier; |
||
| 166 | |||
| 167 |
/*////////////////////////////////////////////////////////////// |
||
| 168 |
EVENTS |
||
| 169 |
//////////////////////////////////////////////////////////////*/ |
||
| 170 |
event PolicyCreated(uint256 version, uint256 timestamp); |
||
| 171 |
event PolicyFrozen(uint256 version, uint256 timestamp); |
||
| 172 |
event PolicyEligibilityUpdated(uint256 version, uint256 timestamp); |
||
| 173 |
event PolicyRatiosUpdated(uint256 version, uint256 timestamp); |
||
| 174 |
event PolicyConcentrationUpdated(uint256 version, uint256 timestamp); |
||
| 175 |
event PolicyAttestationUpdated(uint256 version, uint256 timestamp); |
||
| 176 |
event PolicyCovenantsUpdated(uint256 version, uint256 timestamp); |
||
| 177 |
event LoanTierUpdated(uint256 version, uint8 tierId, uint256 timestamp); |
||
| 178 |
event IndustryExcluded( |
||
| 179 |
uint256 version, |
||
| 180 |
bytes32 industry, |
||
| 181 |
uint256 timestamp |
||
| 182 |
); |
||
| 183 |
event MaxTiersChanged(uint8 maxTiers); |
||
| 184 |
event IndustryIncluded( |
||
| 185 |
uint256 version, |
||
| 186 |
bytes32 industry, |
||
| 187 |
uint256 timestamp |
||
| 188 |
); |
||
| 189 | |||
| 190 |
event PolicyAdminChanged(address newAdmin); |
||
| 191 | |||
| 192 |
event PolicyDocumentSet( |
||
| 193 |
uint256 version, |
||
| 194 |
bytes32 hash, |
||
| 195 |
string uri, |
||
| 196 |
uint256 timestamp |
||
| 197 |
); |
||
| 198 |
event PolicyDeactivated(uint256 version, uint256 timestamp); |
||
| 199 | |||
| 200 |
/*////////////////////////////////////////////////////////////// |
||
| 201 |
CONSTRUCTOR |
||
| 202 |
//////////////////////////////////////////////////////////////*/ |
||
| 203 |
constructor() {
|
||
| 204 |
✓ 1
|
policyAdmin = msg.sender; |
|
| 205 |
} |
||
| 206 | |||
| 207 |
/*////////////////////////////////////////////////////////////// |
||
| 208 |
POLICY CREATION |
||
| 209 |
//////////////////////////////////////////////////////////////*/ |
||
| 210 | |||
| 211 |
function createPolicy(uint256 version) external onlyAdmin {
|
||
| 212 |
✓ 1
|
if (version == 0) {
|
|
| 213 |
revert CreditPolicy__InvalidVersion(); |
||
| 214 |
} |
||
| 215 |
if (policyCreated[version]) {
|
||
| 216 |
revert CreditPolicy__PolicyVersionExists(version); |
||
| 217 |
} |
||
| 218 | |||
| 219 |
policyCreated[version] = true; |
||
| 220 |
✓ 1
|
policyActive[version] = true; |
|
| 221 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 222 |
✓ 1
|
emit PolicyCreated(version, block.timestamp); |
|
| 223 |
} |
||
| 224 | |||
| 225 |
function freezePolicy( |
||
| 226 |
uint256 version |
||
| 227 |
) external onlyAdmin policyExists(version) {
|
||
| 228 |
✓ 1
|
if (policyFrozen[version]) {
|
|
| 229 |
revert CreditPolicy__PolicyFrozen(version); |
||
| 230 |
} |
||
| 231 |
✓ 1
|
if (policyActive[version] == false) {
|
|
| 232 |
revert CreditPolicy__PolicyNotActive(version); |
||
| 233 |
} |
||
| 234 |
if ( |
||
| 235 |
✓ 1
|
!eligibilitySet[version] || |
|
| 236 |
✓ 1
|
!ratiosSet[version] || |
|
| 237 |
✓ 1
|
!concentrationSet[version] || |
|
| 238 |
✓ 1
|
!attestationSet[version] || |
|
| 239 |
✓ 1
|
!covenantsSet[version] || |
|
| 240 |
✓ 1
|
!hasAtLeastOneTier[version] |
|
| 241 |
) {
|
||
| 242 |
revert CreditPolicy__IncompletePolicy(version); |
||
| 243 |
} |
||
| 244 |
✓ 1
|
if (policyDocumentHash[version] == bytes32(0)) {
|
|
| 245 |
revert CreditPolicy__IncompletePolicy(version); |
||
| 246 |
} |
||
| 247 | |||
| 248 |
policyFrozen[version] = true; |
||
| 249 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 250 |
✓ 1
|
emit PolicyFrozen(version, block.timestamp); |
|
| 251 |
} |
||
| 252 | |||
| 253 |
function deActivatePolicy( |
||
| 254 |
uint256 version |
||
| 255 |
) external onlyAdmin policyExists(version) {
|
||
| 256 |
policyActive[version] = false; |
||
| 257 |
lastUpdated[version] = block.timestamp; |
||
| 258 |
emit PolicyDeactivated(version, block.timestamp); |
||
| 259 |
} |
||
| 260 | |||
| 261 |
/*////////////////////////////////////////////////////////////// |
||
| 262 |
ELIGIBILITY UPDATE |
||
| 263 |
//////////////////////////////////////////////////////////////*/ |
||
| 264 |
function updateEligibility( |
||
| 265 |
uint256 version, |
||
| 266 |
EligibilityCriteria calldata data |
||
| 267 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 268 |
✓ 1
|
eligibility[version] = data; |
|
| 269 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 270 |
✓ 1
|
eligibilitySet[version] = true; |
|
| 271 |
✓ 1
|
emit PolicyEligibilityUpdated(version, block.timestamp); |
|
| 272 |
} |
||
| 273 | |||
| 274 |
/*////////////////////////////////////////////////////////////// |
||
| 275 |
RATIOS UPDATE |
||
| 276 |
//////////////////////////////////////////////////////////////*/ |
||
| 277 |
function updateRatios( |
||
| 278 |
uint256 version, |
||
| 279 |
FinancialRatios calldata data |
||
| 280 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 281 |
✓ 1
|
ratios[version] = data; |
|
| 282 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 283 |
✓ 1
|
ratiosSet[version] = true; |
|
| 284 |
✓ 1
|
emit PolicyRatiosUpdated(version, block.timestamp); |
|
| 285 |
} |
||
| 286 | |||
| 287 |
/*////////////////////////////////////////////////////////////// |
||
| 288 |
CONCENTRATION UPDATE |
||
| 289 |
//////////////////////////////////////////////////////////////*/ |
||
| 290 |
function updateConcentration( |
||
| 291 |
uint256 version, |
||
| 292 |
ConcentrationLimits calldata data |
||
| 293 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 294 |
✓ 1
|
concentration[version] = data; |
|
| 295 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 296 |
✓ 1
|
concentrationSet[version] = true; |
|
| 297 |
✓ 1
|
emit PolicyConcentrationUpdated(version, block.timestamp); |
|
| 298 |
} |
||
| 299 | |||
| 300 |
/*////////////////////////////////////////////////////////////// |
||
| 301 |
ATTESTATION UPDATE |
||
| 302 |
//////////////////////////////////////////////////////////////*/ |
||
| 303 |
function updateAttestation( |
||
| 304 |
uint256 version, |
||
| 305 |
AttestationRequirements calldata data |
||
| 306 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 307 |
✓ 1
|
attestation[version] = data; |
|
| 308 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 309 |
✓ 1
|
attestationSet[version] = true; |
|
| 310 |
✓ 1
|
emit PolicyAttestationUpdated(version, block.timestamp); |
|
| 311 |
} |
||
| 312 | |||
| 313 |
/*////////////////////////////////////////////////////////////// |
||
| 314 |
COVENANT UPDATE |
||
| 315 |
//////////////////////////////////////////////////////////////*/ |
||
| 316 |
function updateCovenants( |
||
| 317 |
uint256 version, |
||
| 318 |
MaintenanceCovenants calldata data |
||
| 319 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 320 |
✓ 1
|
covenants[version] = data; |
|
| 321 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 322 |
✓ 1
|
covenantsSet[version] = true; |
|
| 323 |
✓ 1
|
emit PolicyCovenantsUpdated(version, block.timestamp); |
|
| 324 |
} |
||
| 325 | |||
| 326 |
/*////////////////////////////////////////////////////////////// |
||
| 327 |
LOAN TIERS |
||
| 328 |
//////////////////////////////////////////////////////////////*/ |
||
| 329 |
function setLoanTier( |
||
| 330 |
uint256 version, |
||
| 331 |
uint8 tierId, |
||
| 332 |
LoanTier calldata tier |
||
| 333 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 334 |
✓ 1
|
if (tierId >= maxTiers) {
|
|
| 335 |
revert CreditPolicy__InvalidTierCount(tierId); |
||
| 336 |
} |
||
| 337 |
✓ 1
|
loanTiers[version][tierId] = tier; |
|
| 338 |
tierExists[version][tierId] = true; |
||
| 339 |
✓ 1
|
if (tierId >= totalTiers[version]) {
|
|
| 340 |
totalTiers[version] = tierId + 1; |
||
| 341 |
} |
||
| 342 |
✓ 1
|
hasAtLeastOneTier[version] = true; |
|
| 343 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 344 |
✓ 1
|
emit LoanTierUpdated(version, tierId, block.timestamp); |
|
| 345 |
} |
||
| 346 | |||
| 347 |
/*////////////////////////////////////////////////////////////// |
||
| 348 |
INDUSTRY CONTROLS |
||
| 349 |
//////////////////////////////////////////////////////////////*/ |
||
| 350 |
function excludeIndustry( |
||
| 351 |
uint256 version, |
||
| 352 |
bytes32 industry |
||
| 353 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 354 |
if (industry == bytes32(0)) {
|
||
| 355 |
revert CreditPolicy__InvalidIndustryHash(); |
||
| 356 |
} |
||
| 357 |
excludedIndustries[version][industry] = true; |
||
| 358 |
lastUpdated[version] = block.timestamp; |
||
| 359 |
emit IndustryExcluded(version, industry, block.timestamp); |
||
| 360 |
} |
||
| 361 | |||
| 362 |
function includeIndustry( |
||
| 363 |
uint256 version, |
||
| 364 |
bytes32 industry |
||
| 365 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 366 |
if (industry == bytes32(0)) {
|
||
| 367 |
revert CreditPolicy__InvalidIndustryHash(); |
||
| 368 |
} |
||
| 369 |
excludedIndustries[version][industry] = false; |
||
| 370 |
lastUpdated[version] = block.timestamp; |
||
| 371 |
emit IndustryIncluded(version, industry, block.timestamp); |
||
| 372 |
} |
||
| 373 | |||
| 374 |
/*////////////////////////////////////////////////////////////// |
||
| 375 |
DOCUMENT UPDATE |
||
| 376 |
//////////////////////////////////////////////////////////////*/ |
||
| 377 |
function setPolicyDocument( |
||
| 378 |
uint256 version, |
||
| 379 |
bytes32 hash, |
||
| 380 |
string calldata uri |
||
| 381 |
) external onlyAdmin policyExists(version) policyEditable(version) {
|
||
| 382 |
✓ 1
|
policyDocumentHash[version] = hash; |
|
| 383 |
✓ 1
|
policyDocumentURI[version] = uri; |
|
| 384 |
✓ 1
|
lastUpdated[version] = block.timestamp; |
|
| 385 | |||
| 386 |
✓ 1
|
emit PolicyDocumentSet(version, hash, uri, block.timestamp); |
|
| 387 |
} |
||
| 388 | |||
| 389 |
function changePolicyAdmin(address newAdmin) external onlyAdmin {
|
||
| 390 |
if (newAdmin == address(0)) {
|
||
| 391 |
revert CreditPolicy__InvalidAdmin(); |
||
| 392 |
} |
||
| 393 |
policyAdmin = newAdmin; |
||
| 394 | |||
| 395 |
emit PolicyAdminChanged(newAdmin); |
||
| 396 |
} |
||
| 397 | |||
| 398 |
// getters for interface compliance |
||
| 399 | |||
| 400 |
function isPolicyActive(uint256 version) external view returns (bool) {
|
||
| 401 |
return policyActive[version]; |
||
| 402 |
} |
||
| 403 | |||
| 404 |
function isPolicyFrozen(uint256 version) external view returns (bool) {
|
||
| 405 |
return policyFrozen[version]; |
||
| 406 |
} |
||
| 407 | |||
| 408 |
function tierExistsInPolicy( |
||
| 409 |
uint256 version, |
||
| 410 |
uint8 tierId |
||
| 411 |
) external view returns (bool) {
|
||
| 412 |
return tierExists[version][tierId]; |
||
| 413 |
} |
||
| 414 | |||
| 415 |
function setMaxTiers(uint8 _maxTiers) external onlyAdmin {
|
||
| 416 |
✓ 1
|
if (_maxTiers == 255) {
|
|
| 417 |
revert CreditPolicy__InvalidTierCount(_maxTiers); |
||
| 418 |
} |
||
| 419 |
maxTiers = _maxTiers; |
||
| 420 |
✓ 1
|
emit MaxTiersChanged(_maxTiers); |
|
| 421 |
} |
||
| 422 | |||
| 423 |
function getMaxTiers() external view returns (uint8) {
|
||
| 424 |
return maxTiers; |
||
| 425 |
} |
||
| 426 | |||
| 427 |
function isIndustryExcluded( |
||
| 428 |
uint256 version, |
||
| 429 |
bytes32 industry |
||
| 430 |
) external view returns (bool) {
|
||
| 431 |
✓ 82.9K
|
return excludedIndustries[version][industry]; |
|
| 432 |
} |
||
| 433 |
} |
||
| 434 |
66.1%
src/LoanEngine.sol
Lines covered: 84 / 127 (66.1%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 | |||
| 4 |
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||
| 5 |
import {ICreditPolicy} from "./interfaces/ICreditPolicy.sol";
|
||
| 6 |
import {IVerifier} from "./interfaces/IVerifier.sol";
|
||
| 7 |
import {ITranchePool} from "./interfaces/ITranchePool.sol";
|
||
| 8 |
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
|
||
| 9 |
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||
| 10 |
import {TranchePool} from "./TranchePool.sol";
|
||
| 11 | |||
| 12 |
contract LoanEngine is Ownable, ReentrancyGuard {
|
||
| 13 |
using SafeERC20 for IERC20; |
||
| 14 |
/*////////////////////////////////////////////////////////////// |
||
| 15 |
ERRORS |
||
| 16 |
//////////////////////////////////////////////////////////////*/ |
||
| 17 | |||
| 18 |
error LoanEngine__PolicyNotFrozen(uint256 policyVersion); |
||
| 19 |
error LoanEngine__InvalidProof(); |
||
| 20 |
error LoanEngine__LoanTierIsNotInPolicy( |
||
| 21 |
uint256 policyVersion, |
||
| 22 |
uint8 tierId |
||
| 23 |
); |
||
| 24 |
error LoanEngine__InvalidLoanParameters( |
||
| 25 |
uint256 loanId, |
||
| 26 |
uint256 principalIssued, |
||
| 27 |
uint256 aprBps, |
||
| 28 |
uint256 termDays |
||
| 29 |
); |
||
| 30 |
error LoanEngine__MaxOriginationFeeExceeded( |
||
| 31 |
uint256 loanId, |
||
| 32 |
uint256 originationFeeBps, |
||
| 33 |
uint256 maxOriginationFeeBps |
||
| 34 |
); |
||
| 35 |
error LoanEngine__PoolNotDeployed(); |
||
| 36 |
error LoanEngine__InvalidOffRampingEntity(address entity); |
||
| 37 |
error LoanEngine__LoanExists(uint256 loanId); |
||
| 38 |
error LoanEngine__LoanIsNotInCreatedState(uint256 loanId); |
||
| 39 |
error LoanEngine__LoanIsNotActive(uint256 loanId); |
||
| 40 |
error LoanEngine__LoanIsNotDefaulted(uint256 loanId); |
||
| 41 |
error LoanEngine__InvalidRepayment(); |
||
| 42 |
error LoanEngine__ZeroRecovery(); |
||
| 43 |
error LoanEngine__LoanNotRecoverable(uint256 loanId); |
||
| 44 |
error LoanEngine__ZeroLossOnWriteOff(uint256 loanId); |
||
| 45 |
error LoanEngine__InvalidFeeManagerEntity(address manager); |
||
| 46 |
error LoanEngine__InvalidRecoveryAgent(address agent); |
||
| 47 |
error LoanEngine__InvalidRepaymentAgent(address agent); |
||
| 48 |
error LoanEngine__InsufficientPoolLiquidity(); |
||
| 49 |
error LoanEngine__ProofAlreadyUsed(); |
||
| 50 |
modifier isWhiteListedOffRampingEntity(address entity) {
|
||
| 51 |
_isWhiteListedOffRampingEntity(entity); |
||
| 52 |
_; |
||
| 53 |
} |
||
| 54 | |||
| 55 |
function _isWhiteListedOffRampingEntity(address entity) internal view {
|
||
| 56 |
✓ 35.3K
|
if (!whitelistedOffRampingEntities[entity]) {
|
|
| 57 |
revert LoanEngine__InvalidOffRampingEntity(entity); |
||
| 58 |
} |
||
| 59 |
} |
||
| 60 | |||
| 61 |
modifier isWhiteListedRecoveryAgent(address agent) {
|
||
| 62 |
_isWhiteListedRecoveryAgent(agent); |
||
| 63 |
_; |
||
| 64 |
} |
||
| 65 | |||
| 66 |
function _isWhiteListedRecoveryAgent(address agent) internal view {
|
||
| 67 |
if (!whitelistedRecoveryAgents[agent]) {
|
||
| 68 |
revert LoanEngine__InvalidRecoveryAgent(agent); |
||
| 69 |
} |
||
| 70 |
} |
||
| 71 | |||
| 72 |
modifier isWhiteListedRepaymentAgent(address agent) {
|
||
| 73 |
_isWhiteListedRepaymentAgent(agent); |
||
| 74 |
_; |
||
| 75 |
} |
||
| 76 | |||
| 77 |
function _isWhiteListedRepaymentAgent(address agent) internal view {
|
||
| 78 |
✓ 17.2K
|
if (!whitelistedRepaymentAgents[agent]) {
|
|
| 79 |
revert LoanEngine__InvalidRepaymentAgent(agent); |
||
| 80 |
} |
||
| 81 |
} |
||
| 82 | |||
| 83 |
modifier isWhiteListedFeeManager(address manager) {
|
||
| 84 |
_isWhiteListedFeeManager(manager); |
||
| 85 |
_; |
||
| 86 |
} |
||
| 87 | |||
| 88 |
function _isWhiteListedFeeManager(address manager) internal view {
|
||
| 89 |
✓ 35.3K
|
if (!whitelistedFeeManagers[manager]) {
|
|
| 90 |
revert LoanEngine__InvalidFeeManagerEntity(manager); |
||
| 91 |
} |
||
| 92 |
} |
||
| 93 | |||
| 94 |
ICreditPolicy public creditPolicyContract; |
||
| 95 |
IVerifier loanProofVerifier; |
||
| 96 |
ITranchePool tranchePool; |
||
| 97 | |||
| 98 |
mapping(uint256 loanId => Loan) public s_loans; |
||
| 99 |
mapping(uint256 loanId => uint256) public s_originationFees; |
||
| 100 |
mapping(address whitelistedOffRampingEntity => bool) |
||
| 101 |
public whitelistedOffRampingEntities; |
||
| 102 |
mapping(address whiteListedRecoveryAgent => bool) |
||
| 103 |
public whitelistedRecoveryAgents; |
||
| 104 | |||
| 105 |
mapping(address whiteListedRepaymentAgent => bool) |
||
| 106 |
public whitelistedRepaymentAgents; |
||
| 107 | |||
| 108 |
mapping(address whiteListedFeeManager => bool) |
||
| 109 |
public whitelistedFeeManagers; |
||
| 110 |
mapping(bytes32 nullifierHash => bool) public s_nullifierHashes; |
||
| 111 |
✓ 1
|
uint256 public s_nextLoanId = 1; |
|
| 112 |
✓ 82.9K
|
uint256 public s_maxOriginationFeeBps; |
|
| 113 |
address public s_stableCoinAddress; |
||
| 114 |
uint256 public constant STANDARD_BPS = 100; |
||
| 115 |
enum LoanState {
|
||
| 116 |
NONE, |
||
| 117 |
CREATED, |
||
| 118 |
ACTIVE, |
||
| 119 |
REPAID, |
||
| 120 |
DEFAULTED, |
||
| 121 |
WRITTEN_OFF |
||
| 122 |
} |
||
| 123 | |||
| 124 |
struct Loan {
|
||
| 125 |
// Identity |
||
| 126 |
uint256 loanId; |
||
| 127 |
bytes32 borrowerCommitment; |
||
| 128 |
uint256 policyVersion; |
||
| 129 |
uint8 tierId; |
||
| 130 |
// Economics |
||
| 131 |
uint256 principalIssued; |
||
| 132 |
uint256 principalOutstanding; |
||
| 133 |
uint256 aprBps; |
||
| 134 |
uint256 originationFeeBps; |
||
| 135 |
// Interest accounting |
||
| 136 |
uint256 interestAccrued; |
||
| 137 |
uint256 interestPaid; |
||
| 138 |
uint256 lastAccrualTimestamp; |
||
| 139 |
// Timing |
||
| 140 |
uint256 startTimestamp; |
||
| 141 |
uint256 maturityTimestamp; |
||
| 142 |
uint256 termDays; |
||
| 143 |
// State |
||
| 144 |
LoanState state; |
||
| 145 |
uint256 totalRecovered; |
||
| 146 |
// allocation_ratio |
||
| 147 |
uint256 seniorPrincipalAllocated; |
||
| 148 |
uint256 juniorPrincipalAllocated; |
||
| 149 |
} |
||
| 150 | |||
| 151 |
/*////////////////////////////////////////////////////////////// |
||
| 152 |
EVENTS |
||
| 153 |
//////////////////////////////////////////////////////////////*/ |
||
| 154 | |||
| 155 |
event LoanCreated( |
||
| 156 |
uint256 indexed loanId, |
||
| 157 |
bytes32 borrowerCommitment, |
||
| 158 |
uint256 principalIssued, |
||
| 159 |
uint8 tierId, |
||
| 160 |
uint256 timestamp |
||
| 161 |
); |
||
| 162 | |||
| 163 |
event LoanActivated( |
||
| 164 |
uint256 indexed loanId, |
||
| 165 |
uint256 principalIssued, |
||
| 166 |
uint256 timestamp, |
||
| 167 |
uint256 startTimestamp, |
||
| 168 |
uint256 maturityTimestamp |
||
| 169 |
); |
||
| 170 | |||
| 171 |
event LoanRepaid( |
||
| 172 |
uint256 indexed loanId, |
||
| 173 |
uint256 principalRepaid, |
||
| 174 |
uint256 interestRepaid, |
||
| 175 |
uint256 timestamp |
||
| 176 |
); |
||
| 177 | |||
| 178 |
event LoanClosed(uint256 indexed loanId, uint256 timestamp); |
||
| 179 | |||
| 180 |
event LoanDefaulted( |
||
| 181 |
uint256 indexed loanId, |
||
| 182 |
bytes32 reasonHash, |
||
| 183 |
uint256 timestamp |
||
| 184 |
); |
||
| 185 | |||
| 186 |
event LoanWrittenOff(uint256 indexed loanId, uint256 timestamp); |
||
| 187 | |||
| 188 |
event LoanRecovered( |
||
| 189 |
uint256 indexed loanId, |
||
| 190 |
uint256 amount, |
||
| 191 |
uint256 timestamp |
||
| 192 |
); |
||
| 193 | |||
| 194 |
constructor( |
||
| 195 |
address _creditPolicyContract, |
||
| 196 |
address _loanProofVerifier, |
||
| 197 |
uint256 _maxOriginationFeeBps, |
||
| 198 |
address _tranchePool, |
||
| 199 |
address _stableCoinAddress |
||
| 200 |
✓ 1
|
) Ownable(msg.sender) {
|
|
| 201 |
creditPolicyContract = ICreditPolicy(_creditPolicyContract); |
||
| 202 |
✓ 1
|
loanProofVerifier = IVerifier(_loanProofVerifier); |
|
| 203 |
✓ 1
|
s_maxOriginationFeeBps = _maxOriginationFeeBps; |
|
| 204 |
✓ 1
|
tranchePool = ITranchePool(_tranchePool); |
|
| 205 |
✓ 1
|
s_stableCoinAddress = _stableCoinAddress; |
|
| 206 |
} |
||
| 207 | |||
| 208 |
// A notarization step that records a policy-compliant loan intent on-chain |
||
| 209 |
// TODO: public inputs needed to be verified against the contract state |
||
| 210 |
// will be implemented after the public inputs structure is finalized |
||
| 211 |
// preconditions |
||
| 212 |
// borrowerCommitment should match the publicinput commitment |
||
| 213 |
// all the parameteres should match the public inputs |
||
| 214 |
function createLoan( |
||
| 215 |
bytes32 borrowerCommitment, |
||
| 216 |
bytes32 nullifierHash, |
||
| 217 |
uint256 policyVersion, |
||
| 218 |
uint8 tierId, |
||
| 219 |
uint256 principalIssued, |
||
| 220 |
uint256 aprBps, |
||
| 221 |
uint256 originationFeeBps, |
||
| 222 |
uint256 termDays, |
||
| 223 |
bytes32 industry, |
||
| 224 |
bytes calldata proofData, |
||
| 225 |
bytes32[] calldata publicInputs |
||
| 226 |
) external onlyOwner {
|
||
| 227 |
✓ 82.9K
|
if (s_loans[s_nextLoanId].state != LoanState.NONE) {
|
|
| 228 |
revert LoanEngine__LoanExists(s_nextLoanId); |
||
| 229 |
} |
||
| 230 |
// Implementation goes here |
||
| 231 |
✓ 82.9K
|
if (!creditPolicyContract.isPolicyFrozen(policyVersion)) {
|
|
| 232 |
revert LoanEngine__PolicyNotFrozen(policyVersion); |
||
| 233 |
} |
||
| 234 | |||
| 235 |
✓ 82.9K
|
if (creditPolicyContract.isIndustryExcluded(policyVersion, industry)) {
|
|
| 236 |
revert LoanEngine__PolicyNotFrozen(policyVersion); |
||
| 237 |
} |
||
| 238 | |||
| 239 |
✓ 82.9K
|
if (!creditPolicyContract.tierExistsInPolicy(policyVersion, tierId)) {
|
|
| 240 |
revert LoanEngine__LoanTierIsNotInPolicy(policyVersion, tierId); |
||
| 241 |
} |
||
| 242 | |||
| 243 |
✓ 82.9K
|
if (s_nullifierHashes[nullifierHash]) {
|
|
| 244 |
revert LoanEngine__ProofAlreadyUsed(); |
||
| 245 |
} |
||
| 246 | |||
| 247 |
✓ 82.9K
|
if (loanProofVerifier.verify(proofData, publicInputs) == false) {
|
|
| 248 |
revert LoanEngine__InvalidProof(); |
||
| 249 |
} |
||
| 250 | |||
| 251 |
if ( |
||
| 252 |
✓ 82.9K
|
tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED && |
|
| 253 |
✓ 125.3K
|
tranchePool.getPoolState() != TranchePool.PoolState.COMMITED |
|
| 254 |
) {
|
||
| 255 |
revert LoanEngine__PoolNotDeployed(); |
||
| 256 |
} |
||
| 257 | |||
| 258 |
✓ 82.9K
|
if (principalIssued == 0 || aprBps == 0 || termDays == 0) {
|
|
| 259 |
revert LoanEngine__InvalidLoanParameters( |
||
| 260 |
s_nextLoanId, |
||
| 261 |
principalIssued, |
||
| 262 |
aprBps, |
||
| 263 |
termDays |
||
| 264 |
); |
||
| 265 |
} |
||
| 266 | |||
| 267 |
✓ 82.9K
|
if (originationFeeBps > s_maxOriginationFeeBps) {
|
|
| 268 |
revert LoanEngine__MaxOriginationFeeExceeded( |
||
| 269 |
s_nextLoanId, |
||
| 270 |
originationFeeBps, |
||
| 271 |
s_maxOriginationFeeBps |
||
| 272 |
); |
||
| 273 |
} |
||
| 274 | |||
| 275 |
✓ 82.9K
|
if (principalIssued > tranchePool.getTotalIdleValue()) {
|
|
| 276 |
revert LoanEngine__InsufficientPoolLiquidity(); |
||
| 277 |
} |
||
| 278 | |||
| 279 |
✓ 82.9K
|
Loan memory newLoan = Loan({
|
|
| 280 |
loanId: s_nextLoanId, |
||
| 281 |
borrowerCommitment: borrowerCommitment, |
||
| 282 |
policyVersion: policyVersion, |
||
| 283 |
tierId: tierId, |
||
| 284 |
principalIssued: principalIssued, |
||
| 285 |
principalOutstanding: 0, |
||
| 286 |
aprBps: aprBps, |
||
| 287 |
originationFeeBps: originationFeeBps, |
||
| 288 |
interestAccrued: 0, |
||
| 289 |
interestPaid: 0, |
||
| 290 |
lastAccrualTimestamp: 0, |
||
| 291 |
startTimestamp: 0, |
||
| 292 |
maturityTimestamp: 0, |
||
| 293 |
termDays: termDays, |
||
| 294 |
state: LoanState.CREATED, |
||
| 295 |
totalRecovered: 0, |
||
| 296 |
seniorPrincipalAllocated: 0, |
||
| 297 |
juniorPrincipalAllocated: 0 |
||
| 298 |
}); |
||
| 299 | |||
| 300 |
s_loans[s_nextLoanId++] = newLoan; |
||
| 301 |
s_nullifierHashes[nullifierHash] = true; |
||
| 302 | |||
| 303 |
emit LoanCreated( |
||
| 304 |
newLoan.loanId, |
||
| 305 |
borrowerCommitment, |
||
| 306 |
principalIssued, |
||
| 307 |
tierId, |
||
| 308 |
✓ 82.9K
|
block.timestamp |
|
| 309 |
); |
||
| 310 |
} |
||
| 311 | |||
| 312 |
/* |
||
| 313 |
preconditions |
||
| 314 |
- onlyOwner |
||
| 315 |
- loan must already exist |
||
| 316 |
- loan.state == CREATED |
||
| 317 |
*/ |
||
| 318 |
function activateLoan( |
||
| 319 |
uint256 loanId, |
||
| 320 |
address receivingEntity, |
||
| 321 |
address feeManager |
||
| 322 |
) |
||
| 323 |
external |
||
| 324 |
onlyOwner |
||
| 325 |
isWhiteListedOffRampingEntity(receivingEntity) |
||
| 326 |
isWhiteListedFeeManager(feeManager) |
||
| 327 |
nonReentrant |
||
| 328 |
{
|
||
| 329 |
// Implementation goes here |
||
| 330 |
Loan storage loan = s_loans[loanId]; |
||
| 331 | |||
| 332 |
✓ 35.3K
|
if (loan.state != LoanState.CREATED) {
|
|
| 333 |
revert LoanEngine__LoanIsNotInCreatedState(loanId); |
||
| 334 |
} |
||
| 335 |
✓ 35.3K
|
loan.principalOutstanding = loan.principalIssued; |
|
| 336 |
✓ 35.3K
|
loan.lastAccrualTimestamp = block.timestamp; |
|
| 337 |
✓ 35.3K
|
loan.startTimestamp = block.timestamp; |
|
| 338 |
✓ 35.3K
|
loan.maturityTimestamp = block.timestamp + (loan.termDays * 1 days); |
|
| 339 |
✓ 35.3K
|
loan.state = LoanState.ACTIVE; |
|
| 340 | |||
| 341 |
uint256 originationFee = (loan.principalIssued * |
||
| 342 |
✓ 35.3K
|
loan.originationFeeBps) / 10000; |
|
| 343 | |||
| 344 |
s_originationFees[loanId] = originationFee; |
||
| 345 | |||
| 346 |
✓ 35.3K
|
if (loan.principalIssued > tranchePool.getTotalIdleValue()) {
|
|
| 347 |
revert LoanEngine__InsufficientPoolLiquidity(); |
||
| 348 |
} |
||
| 349 | |||
| 350 |
✓ 35.3K
|
uint256 totalDisbursement = loan.principalIssued - originationFee; |
|
| 351 |
✓ 35.3K
|
(uint256 seniorAmount, uint256 juniorAmount, ) = tranchePool |
|
| 352 |
.allocateCapital( |
||
| 353 |
totalDisbursement, |
||
| 354 |
originationFee, |
||
| 355 |
receivingEntity, |
||
| 356 |
feeManager |
||
| 357 |
); |
||
| 358 |
✓ 35.3K
|
loan.seniorPrincipalAllocated = seniorAmount; |
|
| 359 | |||
| 360 |
✓ 35.3K
|
loan.juniorPrincipalAllocated = juniorAmount; |
|
| 361 | |||
| 362 |
✓ 35.3K
|
emit LoanActivated( |
|
| 363 |
loan.loanId, |
||
| 364 |
loan.principalIssued, |
||
| 365 |
block.timestamp, |
||
| 366 |
loan.startTimestamp, |
||
| 367 |
loan.maturityTimestamp |
||
| 368 |
); |
||
| 369 |
} |
||
| 370 | |||
| 371 |
function repayLoan( |
||
| 372 |
uint256 loanId, |
||
| 373 |
uint256 principalAmount, |
||
| 374 |
uint256 interestAmount, |
||
| 375 |
address repaymentAgent |
||
| 376 |
) |
||
| 377 |
external |
||
| 378 |
onlyOwner |
||
| 379 |
isWhiteListedRepaymentAgent(repaymentAgent) |
||
| 380 |
nonReentrant |
||
| 381 |
{
|
||
| 382 |
Loan storage loan = s_loans[loanId]; |
||
| 383 |
✓ 17.2K
|
if (loan.state != LoanState.ACTIVE) {
|
|
| 384 |
revert LoanEngine__LoanIsNotActive(loanId); |
||
| 385 |
} |
||
| 386 | |||
| 387 |
✓ 17.2K
|
uint256 totalPayment = principalAmount + interestAmount; |
|
| 388 |
✓ 17.2K
|
if (totalPayment == 0) {
|
|
| 389 |
revert LoanEngine__InvalidRepayment(); |
||
| 390 |
} |
||
| 391 | |||
| 392 |
✓ 17.2K
|
_accrueInterest(loanId); |
|
| 393 | |||
| 394 |
// 1️⃣ Transfer funds to pool (settlement layer) |
||
| 395 |
✓ 17.2K
|
IERC20(s_stableCoinAddress).safeTransferFrom( |
|
| 396 |
repaymentAgent, |
||
| 397 |
✓ 17.2K
|
address(tranchePool), |
|
| 398 |
✓ 17.2K
|
totalPayment |
|
| 399 |
); |
||
| 400 | |||
| 401 |
// 2️⃣ Interest first |
||
| 402 |
✓ 17.2K
|
uint256 interestDue = loan.interestAccrued; |
|
| 403 |
✓ 17.2K
|
uint256 interestPaid = totalPayment > interestDue |
|
| 404 |
? interestDue |
||
| 405 |
: totalPayment; |
||
| 406 | |||
| 407 |
// 3️⃣ Principal second |
||
| 408 |
✓ 17.2K
|
uint256 remainingForPrincipal = totalPayment - interestPaid; |
|
| 409 |
✓ 17.2K
|
uint256 principalDue = loan.principalOutstanding; |
|
| 410 | |||
| 411 |
✓ 17.2K
|
uint256 principalPaid = remainingForPrincipal > principalDue |
|
| 412 |
? principalDue |
||
| 413 |
: remainingForPrincipal; |
||
| 414 | |||
| 415 |
// 4️⃣ Update loan accounting |
||
| 416 |
✓ 17.2K
|
loan.interestAccrued -= interestPaid; |
|
| 417 |
✓ 17.2K
|
loan.interestPaid += interestPaid; |
|
| 418 |
✓ 17.2K
|
loan.principalOutstanding -= principalPaid; |
|
| 419 | |||
| 420 |
✓ 17.2K
|
bool fullyRepaid = loan.principalOutstanding == 0 && |
|
| 421 |
✓ 9.5K
|
loan.interestAccrued == 0; |
|
| 422 | |||
| 423 |
✓ 17.2K
|
if (fullyRepaid) {
|
|
| 424 |
loan.state = LoanState.REPAID; |
||
| 425 |
} |
||
| 426 | |||
| 427 |
✓ 17.2K
|
tranchePool.onRepayment(principalPaid, interestPaid); |
|
| 428 | |||
| 429 |
emit LoanRepaid( |
||
| 430 |
loan.loanId, |
||
| 431 |
principalPaid, |
||
| 432 |
interestPaid, |
||
| 433 |
✓ 17.2K
|
block.timestamp |
|
| 434 |
); |
||
| 435 | |||
| 436 |
if (fullyRepaid) {
|
||
| 437 |
✓ 9.5K
|
emit LoanClosed(loanId, block.timestamp); |
|
| 438 |
} |
||
| 439 |
} |
||
| 440 | |||
| 441 |
function declareDefault( |
||
| 442 |
uint256 loanId, |
||
| 443 |
bytes32 reasonHash |
||
| 444 |
) external onlyOwner {
|
||
| 445 |
// Implementation goes here |
||
| 446 |
Loan storage loan = s_loans[loanId]; |
||
| 447 |
✓ 18
|
if (loan.state != LoanState.ACTIVE) {
|
|
| 448 |
revert LoanEngine__LoanIsNotActive(loanId); |
||
| 449 |
} |
||
| 450 |
✓ 18
|
_accrueInterest(loanId); |
|
| 451 |
loan.state = LoanState.DEFAULTED; |
||
| 452 |
✓ 18
|
emit LoanDefaulted(loanId, reasonHash, block.timestamp); |
|
| 453 |
} |
||
| 454 | |||
| 455 |
function writeOffLoan(uint256 loanId) external onlyOwner {
|
||
| 456 |
// Implementation goes here |
||
| 457 |
Loan storage loan = s_loans[loanId]; |
||
| 458 |
✓ 5
|
if (loan.state != LoanState.DEFAULTED) {
|
|
| 459 |
revert LoanEngine__LoanIsNotDefaulted(loanId); |
||
| 460 |
} |
||
| 461 |
✓ 5
|
uint256 loss = loan.principalOutstanding; |
|
| 462 |
✓ 5
|
uint256 interestAccrued = loan.interestAccrued; |
|
| 463 |
✓ 5
|
if (loss == 0) {
|
|
| 464 |
revert LoanEngine__ZeroLossOnWriteOff(loanId); |
||
| 465 |
} |
||
| 466 | |||
| 467 |
loan.principalOutstanding = 0; |
||
| 468 |
loan.interestAccrued = 0; |
||
| 469 |
loan.state = LoanState.WRITTEN_OFF; |
||
| 470 |
✓ 5
|
tranchePool.onLoss(loss, interestAccrued); |
|
| 471 |
✓ 5
|
emit LoanWrittenOff(loanId, block.timestamp); |
|
| 472 |
} |
||
| 473 | |||
| 474 |
function recoverLoan( |
||
| 475 |
uint256 loanId, |
||
| 476 |
uint256 amount, |
||
| 477 |
address recoveryAgent |
||
| 478 |
) external onlyOwner isWhiteListedRecoveryAgent(recoveryAgent) {
|
||
| 479 |
Loan storage loan = s_loans[loanId]; |
||
| 480 |
if (loan.state != LoanState.WRITTEN_OFF) {
|
||
| 481 |
revert LoanEngine__LoanNotRecoverable(loanId); |
||
| 482 |
} |
||
| 483 |
if (amount == 0) {
|
||
| 484 |
revert LoanEngine__ZeroRecovery(); |
||
| 485 |
} |
||
| 486 |
loan.totalRecovered += amount; |
||
| 487 |
IERC20(s_stableCoinAddress).safeTransferFrom( |
||
| 488 |
recoveryAgent, |
||
| 489 |
address(tranchePool), |
||
| 490 |
amount |
||
| 491 |
); |
||
| 492 |
tranchePool.onRecovery(amount); |
||
| 493 |
emit LoanRecovered(loanId, amount, block.timestamp); |
||
| 494 |
} |
||
| 495 | |||
| 496 |
function _accrueInterest(uint256 loanId) internal {
|
||
| 497 |
// Implementation goes here |
||
| 498 |
✓ 17.3K
|
Loan storage loan = s_loans[loanId]; |
|
| 499 |
✓ 17.3K
|
if (loan.state != LoanState.ACTIVE) {
|
|
| 500 |
revert LoanEngine__LoanIsNotActive(loanId); |
||
| 501 |
} |
||
| 502 |
✓ 17.3K
|
uint256 timeElapsed = block.timestamp - loan.lastAccrualTimestamp; |
|
| 503 |
✓ 17.3K
|
if (loan.principalOutstanding == 0) {
|
|
| 504 |
loan.lastAccrualTimestamp = block.timestamp; |
||
| 505 |
return; |
||
| 506 |
} |
||
| 507 | |||
| 508 |
uint256 interest = (loan.principalOutstanding * |
||
| 509 |
✓ 17.3K
|
loan.aprBps * |
|
| 510 |
✓ 17.3K
|
timeElapsed) / (365 days * 10_000); |
|
| 511 | |||
| 512 |
✓ 17.3K
|
if (interest > 0) {
|
|
| 513 |
✓ 17.1K
|
loan.interestAccrued += interest; |
|
| 514 |
✓ 17.1K
|
uint256 totalAllocated = loan.principalIssued; |
|
| 515 |
uint256 seniorInterest = (interest * |
||
| 516 |
✓ 17.1K
|
loan.seniorPrincipalAllocated) / totalAllocated; |
|
| 517 | |||
| 518 |
uint256 juniorInterest = (interest * |
||
| 519 |
✓ 17.1K
|
loan.juniorPrincipalAllocated) / totalAllocated; |
|
| 520 |
✓ 17.1K
|
tranchePool.onInterestAccrued( |
|
| 521 |
interest, |
||
| 522 |
seniorInterest, |
||
| 523 |
juniorInterest |
||
| 524 |
); |
||
| 525 |
} |
||
| 526 |
loan.lastAccrualTimestamp = block.timestamp; |
||
| 527 |
} |
||
| 528 | |||
| 529 |
// setters for contract management |
||
| 530 | |||
| 531 |
function setMaxOriginationFeeBps( |
||
| 532 |
uint256 _maxOriginationFeeBps |
||
| 533 |
) external onlyOwner {
|
||
| 534 |
✓ 1
|
s_maxOriginationFeeBps = _maxOriginationFeeBps; |
|
| 535 |
} |
||
| 536 | |||
| 537 |
function setWhitelistedOffRampingEntity( |
||
| 538 |
address entity, |
||
| 539 |
bool isWhitelisted |
||
| 540 |
) external onlyOwner {
|
||
| 541 |
✓ 1
|
whitelistedOffRampingEntities[entity] = isWhitelisted; |
|
| 542 |
} |
||
| 543 | |||
| 544 |
function setWhitelistedRecoveryAgent( |
||
| 545 |
address agent, |
||
| 546 |
bool isWhitelisted |
||
| 547 |
) external onlyOwner {
|
||
| 548 |
✓ 1
|
whitelistedRecoveryAgents[agent] = isWhitelisted; |
|
| 549 |
} |
||
| 550 | |||
| 551 |
function setWhitelistedRepaymentAgent( |
||
| 552 |
address agent, |
||
| 553 |
bool isWhitelisted |
||
| 554 |
) external onlyOwner {
|
||
| 555 |
✓ 1
|
whitelistedRepaymentAgents[agent] = isWhitelisted; |
|
| 556 |
} |
||
| 557 | |||
| 558 |
function setWhitelistedFeeManager( |
||
| 559 |
address manager, |
||
| 560 |
bool isWhitelisted |
||
| 561 |
) external onlyOwner {
|
||
| 562 |
✓ 1
|
whitelistedFeeManagers[manager] = isWhitelisted; |
|
| 563 |
} |
||
| 564 | |||
| 565 |
function getMaxOriginationFeeBps() external view returns (uint256) {
|
||
| 566 |
return s_maxOriginationFeeBps; |
||
| 567 |
} |
||
| 568 | |||
| 569 |
function getNextLoanId() external view returns (uint256) {
|
||
| 570 |
✓ 861.8K
|
return s_nextLoanId; |
|
| 571 |
} |
||
| 572 | |||
| 573 |
function getLoanDetails( |
||
| 574 |
uint256 loanId |
||
| 575 |
) external view returns (Loan memory) {
|
||
| 576 |
return s_loans[loanId]; |
||
| 577 |
} |
||
| 578 |
} |
||
| 579 |
51.9%
src/TranchePool.sol
Lines covered: 219 / 422 (51.9%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 | |||
| 4 |
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
||
| 5 |
import {SafeERC20, IERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
||
| 6 | |||
| 7 |
contract TranchePool is Ownable {
|
||
| 8 |
using SafeERC20 for IERC20; |
||
| 9 | |||
| 10 |
// Errors |
||
| 11 |
error TranchePool__NotWhiteListed(address user); |
||
| 12 |
error TranchePool__LessThanDepositThreshold(uint256 amount); |
||
| 13 |
error TranchePool__InvalidAllocationRatio(); |
||
| 14 |
error TranchePool__InsufficientLiquidity(); |
||
| 15 |
error TranchePool__InsufficientShares(); |
||
| 16 |
error TranchePool__ZeroWithdrawal(); |
||
| 17 |
error TranchePool__NotWhiteListedForEquityTranche(address user); |
||
| 18 |
error TranchePool__InvalidTransferAmount(uint256 amount); |
||
| 19 |
error TranchePool__InvalidCaller(address user); |
||
| 20 |
error TranchePool__ZeroAPRError(); |
||
| 21 |
error TranchePool__LossExceededCapital(uint256 remaining); |
||
| 22 |
error TranchePool__ZeroSharesMinted(); |
||
| 23 |
error TranchePool__PoolIsNotOpen(); |
||
| 24 |
error TranchePool__InvalidStateTransition(PoolState state); |
||
| 25 |
error TranchePool__WithdrawNotAllowed(PoolState state); |
||
| 26 |
error TranchePool__ZeroValueError(); |
||
| 27 |
error TranchePool__MaxDepositCapExceeded(uint256 maxCap, uint256 amount); |
||
| 28 |
error TranchePool__PoolIsNotCommited(); |
||
| 29 |
error TranchePool__PrincipalRepaymentExceeded(); |
||
| 30 |
error TranchePool__ZeroAddressError(); |
||
| 31 |
error TranchePool__DeployedCapitalExists(); |
||
| 32 |
error TranchePool__InvalidMaxCapAmount(); |
||
| 33 |
error TranchePool__InvalidMinDepositAmount(); |
||
| 34 |
error TranchePool__InterestNotClaimed(); |
||
| 35 |
// Events |
||
| 36 | |||
| 37 |
event PoolStateUpdated(PoolState newState); |
||
| 38 | |||
| 39 |
event LossAllocated( |
||
| 40 |
uint256 seniorLoss, |
||
| 41 |
uint256 juniorLoss, |
||
| 42 |
uint256 equityLoss |
||
| 43 |
); |
||
| 44 |
event WithdrawnFromSeniorTranche( |
||
| 45 |
address indexed user, |
||
| 46 |
uint256 amount, |
||
| 47 |
uint256 sharesBurned, |
||
| 48 |
uint256 time |
||
| 49 |
); |
||
| 50 |
event WithdrawnFromJuniorTranche( |
||
| 51 |
address indexed user, |
||
| 52 |
uint256 amount, |
||
| 53 |
uint256 sharesBurned, |
||
| 54 |
uint256 time |
||
| 55 |
); |
||
| 56 |
event WithdrawnFromEquityTranche( |
||
| 57 |
address indexed user, |
||
| 58 |
uint256 amount, |
||
| 59 |
uint256 sharesBurned, |
||
| 60 |
uint256 time |
||
| 61 |
); |
||
| 62 | |||
| 63 |
event FundsDepositedToSeniorTranche( |
||
| 64 |
address indexed user, |
||
| 65 |
uint256 amount, |
||
| 66 |
uint256 shares, |
||
| 67 |
uint256 time |
||
| 68 |
); |
||
| 69 |
event FundsDepositedToJuniorTranche( |
||
| 70 |
address indexed user, |
||
| 71 |
uint256 amount, |
||
| 72 |
uint256 shares, |
||
| 73 |
uint256 time |
||
| 74 |
); |
||
| 75 |
event FundsDepositedToEquityTranche( |
||
| 76 |
address indexed user, |
||
| 77 |
uint256 amount, |
||
| 78 |
uint256 shares, |
||
| 79 |
uint256 time |
||
| 80 |
); |
||
| 81 |
event CapitalAllocated( |
||
| 82 |
uint256 seniorAmount, |
||
| 83 |
uint256 juniorAmount, |
||
| 84 |
uint256 equityAmount, |
||
| 85 |
uint256 time |
||
| 86 |
); |
||
| 87 |
event RecoverAmountTransferredToTranchePool( |
||
| 88 |
uint256 amount, |
||
| 89 |
uint256 timeStamp |
||
| 90 |
); |
||
| 91 |
event ProfitTransferredToTranchePool(uint256 amount, uint256 timeStamp); |
||
| 92 |
event CapitalAllocationFactorUpdatedSenior(uint256 newFactor); |
||
| 93 |
event CapitalAllocationFactorUpdatedJunior(uint256 newFactor); |
||
| 94 | |||
| 95 |
enum PoolState {
|
||
| 96 |
OPEN, // deposits allowed |
||
| 97 |
COMMITED, |
||
| 98 |
DEPLOYED, // capital deployed, deposits paused |
||
| 99 |
CLOSED // withdrawals only |
||
| 100 |
} |
||
| 101 | |||
| 102 |
// Whitelist |
||
| 103 |
mapping(address => bool) public whiteListedLps; |
||
| 104 |
mapping(address => bool) public whiteListedForEquityTranche; |
||
| 105 | |||
| 106 |
// Shares tracking (instead of amounts) |
||
| 107 |
mapping(address => uint256) public s_seniorTrancheShares; |
||
| 108 |
mapping(address => uint256) public s_juniorTrancheShares; |
||
| 109 |
mapping(address => uint256) public s_equityTrancheShares; |
||
| 110 | |||
| 111 |
uint256 public s_totalSeniorShares; |
||
| 112 |
uint256 public s_totalJuniorShares; |
||
| 113 |
uint256 public s_totalEquityShares; |
||
| 114 | |||
| 115 |
// Total value in each tranche (this decreases when capital is allocated) |
||
| 116 |
// why some part of the capital should stay idle. |
||
| 117 |
// 1. Liquidity and operational buffer. |
||
| 118 |
// |
||
| 119 |
uint256 public s_seniorTrancheIdleValue; |
||
| 120 |
uint256 public s_juniorTrancheIdleValue; |
||
| 121 |
uint256 public s_equityTrancheIdleValue; |
||
| 122 | |||
| 123 |
✓ 35.8K
|
uint256 public s_seniorTrancheDeployedValue; |
|
| 124 |
uint256 public s_juniorTrancheDeployedValue; |
||
| 125 |
uint256 public s_equityTrancheDeployedValue; |
||
| 126 | |||
| 127 |
// Minimum deposits |
||
| 128 |
✓ 58.3K
|
uint256 public s_minimumDepositAmountSeniorTranche; |
|
| 129 |
uint256 public s_minimumDepositAmountJuniorTranche; |
||
| 130 |
uint256 public s_minimumDepositAmountEquityTranche; |
||
| 131 | |||
| 132 |
// CHANGED: global interest index (scaled) |
||
| 133 |
uint256 public seniorInterestIndex; // 1e18 precision |
||
| 134 |
uint256 public juniorInterestIndex; // 1e18 precision |
||
| 135 |
uint256 public equityInterestIndex; // 1e18 precision |
||
| 136 | |||
| 137 |
// CHANGED: per-user last claimed index |
||
| 138 |
mapping(address => uint256) public seniorUserIndex; |
||
| 139 |
mapping(address => uint256) public juniorUserIndex; |
||
| 140 |
mapping(address => uint256) public equityUserIndex; |
||
| 141 | |||
| 142 |
// Stable coin |
||
| 143 |
address public s_stableCoin; |
||
| 144 |
address public loanEngine; |
||
| 145 | |||
| 146 |
// Capital allocation factor (e.g., 80 for 80% senior, 15% junior, 5% equity) |
||
| 147 |
uint256 public s_capital_allocation_factor_senior; |
||
| 148 |
uint256 public s_capital_allocation_factor_junior; |
||
| 149 | |||
| 150 |
uint256 public s_senior_apr; |
||
| 151 |
uint256 public s_target_junior_apr; |
||
| 152 | |||
| 153 |
uint256 public seniorAccruedInterest; |
||
| 154 |
uint256 public juniorAccruedInterest; |
||
| 155 |
uint256 public equityAccruedInterest; |
||
| 156 | |||
| 157 |
✓ 94.1K
|
uint256 public s_seniorTrancheMaxCap; |
|
| 158 |
✓ 85.6K
|
uint256 public s_juniorTrancheMaxCap; |
|
| 159 |
uint256 public s_equityTrancheMaxCap; |
||
| 160 | |||
| 161 |
uint256 public s_protocolRevenue; |
||
| 162 |
uint256 public s_totalDeposited; |
||
| 163 |
uint256 public s_totalLoss; |
||
| 164 |
uint256 public s_totalRecovered; |
||
| 165 | |||
| 166 |
✓ 894.4K
|
PoolState public poolState = PoolState.OPEN; |
|
| 167 | |||
| 168 |
uint256 public seniorPrincipalShortfall; |
||
| 169 |
uint256 public juniorPrincipalShortfall; |
||
| 170 |
uint256 public equityPrincipalShortfall; |
||
| 171 | |||
| 172 |
uint256 public s_totalUnclaimedInterest; |
||
| 173 | |||
| 174 |
modifier isWhiteListed(address user) {
|
||
| 175 |
✓ 22.5K
|
_isWhiteListed(user); |
|
| 176 |
_; |
||
| 177 |
} |
||
| 178 | |||
| 179 |
function _isWhiteListed(address user) internal view {
|
||
| 180 |
✓ 44.8K
|
if (!whiteListedLps[user]) {
|
|
| 181 |
revert TranchePool__NotWhiteListed(user); |
||
| 182 |
} |
||
| 183 |
} |
||
| 184 | |||
| 185 |
modifier onlyLoanEngine(address user) {
|
||
| 186 |
✓ 35.3K
|
_onlyLoanEngine(user); |
|
| 187 |
✓ 17.1K
|
_; |
|
| 188 |
} |
||
| 189 | |||
| 190 |
function _onlyLoanEngine(address user) internal view {
|
||
| 191 |
✓ 69.6K
|
if (user != loanEngine) {
|
|
| 192 |
revert TranchePool__InvalidCaller(user); |
||
| 193 |
} |
||
| 194 |
} |
||
| 195 | |||
| 196 |
modifier isWhiteListedForEquityTranche(address user) {
|
||
| 197 |
✓ 22.2K
|
_isWhiteListedForEquityTranche(user); |
|
| 198 |
_; |
||
| 199 |
} |
||
| 200 | |||
| 201 |
function _isWhiteListedForEquityTranche(address user) internal view {
|
||
| 202 |
✓ 22.2K
|
if (!whiteListedForEquityTranche[user]) {
|
|
| 203 |
revert TranchePool__NotWhiteListedForEquityTranche(user); |
||
| 204 |
} |
||
| 205 |
} |
||
| 206 | |||
| 207 |
✓ 1
|
constructor(address stableCoin_) Ownable(msg.sender) {
|
|
| 208 |
✓ 1
|
s_stableCoin = stableCoin_; |
|
| 209 |
✓ 1
|
seniorInterestIndex = 1e18; |
|
| 210 |
✓ 1
|
juniorInterestIndex = 1e18; |
|
| 211 |
✓ 1
|
equityInterestIndex = 1e18; |
|
| 212 |
} |
||
| 213 | |||
| 214 |
function depositSeniorTranche( |
||
| 215 |
uint256 amount |
||
| 216 |
✓ 22.5K
|
) external isWhiteListed(msg.sender) {
|
|
| 217 |
// q: wy we need a minimum deposit for a tranche? |
||
| 218 |
// a: in book. |
||
| 219 |
✓ 22.5K
|
if (poolState != PoolState.OPEN) {
|
|
| 220 |
revert TranchePool__PoolIsNotOpen(); |
||
| 221 |
} |
||
| 222 |
✓ 22.5K
|
if (amount == 0) {
|
|
| 223 |
revert TranchePool__ZeroValueError(); |
||
| 224 |
} |
||
| 225 |
✓ 22.5K
|
if (amount < s_minimumDepositAmountSeniorTranche) {
|
|
| 226 |
revert TranchePool__LessThanDepositThreshold(amount); |
||
| 227 |
} |
||
| 228 |
// why there is a max cap exists? |
||
| 229 |
// |
||
| 230 |
// 1. to prevent the liquidity from sitting idle |
||
| 231 |
// |
||
| 232 |
✓ 22.5K
|
if (amount + s_seniorTrancheIdleValue > s_seniorTrancheMaxCap) {
|
|
| 233 |
revert TranchePool__MaxDepositCapExceeded( |
||
| 234 |
s_seniorTrancheMaxCap, |
||
| 235 |
amount |
||
| 236 |
); |
||
| 237 |
} |
||
| 238 | |||
| 239 |
// Calculate shares to mint |
||
| 240 |
// invariant: the total shares == idle value because the deposit is allowed only when |
||
| 241 |
// the pool is open and once the pool is moved to a new state new deposits are not allowed |
||
| 242 |
// so what shares == amount holding 1:1 is valid and is not affecting or opening any attack vectors. |
||
| 243 |
uint256 shares = amount; |
||
| 244 | |||
| 245 |
✓ 22.5K
|
IERC20(s_stableCoin).safeTransferFrom( |
|
| 246 |
msg.sender, |
||
| 247 |
✓ 22.5K
|
address(this), |
|
| 248 |
✓ 22.5K
|
amount |
|
| 249 |
); |
||
| 250 | |||
| 251 |
✓ 22.5K
|
s_seniorTrancheShares[msg.sender] += shares; |
|
| 252 |
✓ 22.5K
|
s_totalSeniorShares += shares; |
|
| 253 |
✓ 22.5K
|
s_seniorTrancheIdleValue += amount; |
|
| 254 |
✓ 22.5K
|
seniorUserIndex[msg.sender] = seniorInterestIndex; |
|
| 255 |
✓ 22.5K
|
s_totalDeposited += amount; |
|
| 256 |
emit FundsDepositedToSeniorTranche( |
||
| 257 |
msg.sender, |
||
| 258 |
amount, |
||
| 259 |
shares, |
||
| 260 |
✓ 22.5K
|
block.timestamp |
|
| 261 |
); |
||
| 262 |
} |
||
| 263 | |||
| 264 |
function depositJuniorTranche( |
||
| 265 |
uint256 amount |
||
| 266 |
✓ 22.3K
|
) external isWhiteListed(msg.sender) {
|
|
| 267 |
✓ 22.3K
|
if (poolState != PoolState.OPEN) {
|
|
| 268 |
revert TranchePool__PoolIsNotOpen(); |
||
| 269 |
} |
||
| 270 |
✓ 22.3K
|
if (amount < s_minimumDepositAmountJuniorTranche) {
|
|
| 271 |
revert TranchePool__LessThanDepositThreshold(amount); |
||
| 272 |
} |
||
| 273 | |||
| 274 |
✓ 22.3K
|
if (amount + s_juniorTrancheIdleValue > s_juniorTrancheMaxCap) {
|
|
| 275 |
revert TranchePool__MaxDepositCapExceeded( |
||
| 276 |
s_juniorTrancheMaxCap, |
||
| 277 |
amount |
||
| 278 |
); |
||
| 279 |
} |
||
| 280 | |||
| 281 |
uint256 shares = amount; |
||
| 282 | |||
| 283 |
✓ 22.3K
|
IERC20(s_stableCoin).safeTransferFrom( |
|
| 284 |
msg.sender, |
||
| 285 |
✓ 22.3K
|
address(this), |
|
| 286 |
✓ 22.3K
|
amount |
|
| 287 |
); |
||
| 288 | |||
| 289 |
✓ 22.3K
|
s_juniorTrancheShares[msg.sender] += shares; |
|
| 290 |
✓ 22.3K
|
s_totalJuniorShares += shares; |
|
| 291 |
✓ 22.3K
|
s_juniorTrancheIdleValue += amount; |
|
| 292 |
✓ 22.3K
|
juniorUserIndex[msg.sender] = juniorInterestIndex; |
|
| 293 |
✓ 22.3K
|
s_totalDeposited += amount; |
|
| 294 | |||
| 295 |
emit FundsDepositedToJuniorTranche( |
||
| 296 |
msg.sender, |
||
| 297 |
amount, |
||
| 298 |
shares, |
||
| 299 |
✓ 22.3K
|
block.timestamp |
|
| 300 |
); |
||
| 301 |
} |
||
| 302 | |||
| 303 |
function depositEquityTranche( |
||
| 304 |
uint256 amount |
||
| 305 |
✓ 22.2K
|
) external isWhiteListedForEquityTranche(msg.sender) {
|
|
| 306 |
✓ 22.2K
|
if (poolState != PoolState.OPEN) {
|
|
| 307 |
revert TranchePool__PoolIsNotOpen(); |
||
| 308 |
} |
||
| 309 |
✓ 22.2K
|
if (amount < s_minimumDepositAmountEquityTranche) {
|
|
| 310 |
revert TranchePool__LessThanDepositThreshold(amount); |
||
| 311 |
} |
||
| 312 | |||
| 313 |
✓ 22.2K
|
if (amount + s_equityTrancheIdleValue > s_equityTrancheMaxCap) {
|
|
| 314 |
revert TranchePool__MaxDepositCapExceeded( |
||
| 315 |
s_equityTrancheMaxCap, |
||
| 316 |
amount |
||
| 317 |
); |
||
| 318 |
} |
||
| 319 | |||
| 320 |
uint256 shares = amount; |
||
| 321 | |||
| 322 |
✓ 22.2K
|
IERC20(s_stableCoin).safeTransferFrom( |
|
| 323 |
msg.sender, |
||
| 324 |
✓ 22.2K
|
address(this), |
|
| 325 |
✓ 22.2K
|
amount |
|
| 326 |
); |
||
| 327 |
✓ 22.2K
|
s_equityTrancheShares[msg.sender] += shares; |
|
| 328 |
✓ 22.2K
|
s_totalEquityShares += shares; |
|
| 329 |
✓ 22.2K
|
s_equityTrancheIdleValue += amount; |
|
| 330 |
✓ 22.2K
|
equityUserIndex[msg.sender] = equityInterestIndex; |
|
| 331 |
✓ 22.2K
|
s_totalDeposited += amount; |
|
| 332 | |||
| 333 |
emit FundsDepositedToEquityTranche( |
||
| 334 |
msg.sender, |
||
| 335 |
amount, |
||
| 336 |
shares, |
||
| 337 |
✓ 22.2K
|
block.timestamp |
|
| 338 |
); |
||
| 339 |
} |
||
| 340 | |||
| 341 |
/** |
||
| 342 |
* @notice Allocate capital according to the 80/20 split |
||
| 343 |
* @param totalDisbursement Total amount to allocate from the pool |
||
| 344 |
* @param fees Total fees to be collected |
||
| 345 |
*/ |
||
| 346 |
function allocateCapital( |
||
| 347 |
uint256 totalDisbursement, |
||
| 348 |
uint256 fees, |
||
| 349 |
address deployer, |
||
| 350 |
address feeManager |
||
| 351 |
✓ 35.3K
|
) external onlyLoanEngine(msg.sender) returns (uint256, uint256, uint256) {
|
|
| 352 |
if ( |
||
| 353 |
✓ 35.3K
|
poolState != PoolState.COMMITED && poolState != PoolState.DEPLOYED |
|
| 354 |
) {
|
||
| 355 |
revert TranchePool__PoolIsNotCommited(); |
||
| 356 |
} |
||
| 357 | |||
| 358 |
✓ 35.3K
|
uint256 totalAmount = totalDisbursement + fees; |
|
| 359 | |||
| 360 |
// Global liquidity check |
||
| 361 |
✓ 35.3K
|
uint256 totalIdle = s_seniorTrancheIdleValue + |
|
| 362 |
✓ 35.3K
|
s_juniorTrancheIdleValue + |
|
| 363 |
✓ 35.3K
|
s_equityTrancheIdleValue; |
|
| 364 | |||
| 365 |
✓ 35.3K
|
if (totalAmount > totalIdle) {
|
|
| 366 |
revert TranchePool__InsufficientLiquidity(); |
||
| 367 |
} |
||
| 368 | |||
| 369 |
uint256 targetSenior = (totalAmount * |
||
| 370 |
✓ 35.3K
|
s_capital_allocation_factor_senior) / 100; |
|
| 371 | |||
| 372 |
uint256 targetJunior = (totalAmount * |
||
| 373 |
✓ 35.3K
|
s_capital_allocation_factor_junior) / 100; |
|
| 374 | |||
| 375 |
✓ 35.3K
|
uint256 targetEquity = totalAmount - targetSenior - targetJunior; |
|
| 376 | |||
| 377 |
✓ 35.3K
|
uint256 seniorAmount = _minimum(targetSenior, s_seniorTrancheIdleValue); |
|
| 378 | |||
| 379 |
✓ 35.3K
|
uint256 juniorAmount = _minimum(targetJunior, s_juniorTrancheIdleValue); |
|
| 380 | |||
| 381 |
✓ 35.3K
|
uint256 equityAmount = _minimum(targetEquity, s_equityTrancheIdleValue); |
|
| 382 | |||
| 383 |
✓ 35.3K
|
uint256 allocated = seniorAmount + juniorAmount + equityAmount; |
|
| 384 | |||
| 385 |
✓ 35.3K
|
uint256 remaining = totalAmount - allocated; |
|
| 386 | |||
| 387 |
// Equity absorbs first |
||
| 388 |
✓ 35.3K
|
if (remaining > 0 && s_equityTrancheIdleValue > equityAmount) {
|
|
| 389 |
uint256 extra = _minimum( |
||
| 390 |
remaining, |
||
| 391 |
✓ 8.2K
|
s_equityTrancheIdleValue - equityAmount |
|
| 392 |
); |
||
| 393 |
✓ 8.2K
|
equityAmount += extra; |
|
| 394 |
✓ 8.2K
|
remaining -= extra; |
|
| 395 |
} |
||
| 396 | |||
| 397 |
// Junior absorbs next |
||
| 398 |
✓ 35.3K
|
if (remaining > 0 && s_juniorTrancheIdleValue > juniorAmount) {
|
|
| 399 |
uint256 extra = _minimum( |
||
| 400 |
remaining, |
||
| 401 |
✓ 8.2K
|
s_juniorTrancheIdleValue - juniorAmount |
|
| 402 |
); |
||
| 403 |
✓ 8.2K
|
juniorAmount += extra; |
|
| 404 |
✓ 8.2K
|
remaining -= extra; |
|
| 405 |
} |
||
| 406 | |||
| 407 |
// Senior absorbs last |
||
| 408 |
✓ 35.3K
|
if (remaining > 0 && s_seniorTrancheIdleValue > seniorAmount) {
|
|
| 409 |
uint256 extra = _minimum( |
||
| 410 |
remaining, |
||
| 411 |
✓ 10.5K
|
s_seniorTrancheIdleValue - seniorAmount |
|
| 412 |
); |
||
| 413 |
✓ 18.7K
|
seniorAmount += extra; |
|
| 414 |
✓ 10.5K
|
remaining -= extra; |
|
| 415 |
} |
||
| 416 | |||
| 417 |
// Final safety check |
||
| 418 |
✓ 35.3K
|
if (remaining > 0) {
|
|
| 419 |
revert TranchePool__InsufficientLiquidity(); |
||
| 420 |
} |
||
| 421 | |||
| 422 |
if (poolState == PoolState.COMMITED) {
|
||
| 423 |
✓ 21.2K
|
poolState = PoolState.DEPLOYED; |
|
| 424 |
✓ 21.2K
|
emit PoolStateUpdated(PoolState.DEPLOYED); |
|
| 425 |
} |
||
| 426 | |||
| 427 |
✓ 35.3K
|
s_seniorTrancheIdleValue -= seniorAmount; |
|
| 428 |
✓ 35.3K
|
s_juniorTrancheIdleValue -= juniorAmount; |
|
| 429 |
✓ 35.3K
|
s_equityTrancheIdleValue -= equityAmount; |
|
| 430 | |||
| 431 |
✓ 35.3K
|
s_seniorTrancheDeployedValue += seniorAmount; |
|
| 432 |
✓ 35.3K
|
s_juniorTrancheDeployedValue += juniorAmount; |
|
| 433 |
✓ 35.3K
|
s_equityTrancheDeployedValue += equityAmount; |
|
| 434 | |||
| 435 |
✓ 35.3K
|
IERC20(s_stableCoin).safeTransfer(deployer, totalDisbursement); |
|
| 436 | |||
| 437 |
✓ 35.3K
|
if (fees > 0) {
|
|
| 438 |
✓ 35.3K
|
IERC20(s_stableCoin).safeTransfer(feeManager, fees); |
|
| 439 |
} |
||
| 440 | |||
| 441 |
emit CapitalAllocated( |
||
| 442 |
seniorAmount, |
||
| 443 |
juniorAmount, |
||
| 444 |
equityAmount, |
||
| 445 |
✓ 35.3K
|
block.timestamp |
|
| 446 |
); |
||
| 447 |
return (seniorAmount, juniorAmount, equityAmount); |
||
| 448 |
} |
||
| 449 | |||
| 450 |
function onInterestAccrued( |
||
| 451 |
uint256 interestAmount, |
||
| 452 |
uint256 seniorInterest, |
||
| 453 |
uint256 juniorInterest |
||
| 454 |
✓ 17.1K
|
) external onlyLoanEngine(msg.sender) {
|
|
| 455 |
✓ 17.1K
|
if (interestAmount == 0) return; |
|
| 456 | |||
| 457 |
✓ 17.1K
|
seniorAccruedInterest += seniorInterest; |
|
| 458 |
✓ 17.1K
|
juniorAccruedInterest += juniorInterest; |
|
| 459 |
✓ 17.1K
|
equityAccruedInterest += (interestAmount - |
|
| 460 |
seniorInterest - |
||
| 461 |
juniorInterest); |
||
| 462 |
} |
||
| 463 | |||
| 464 |
function onRepayment( |
||
| 465 |
uint256 principalRepaid, |
||
| 466 |
uint256 interestRepaid |
||
| 467 |
✓ 17.2K
|
) external onlyLoanEngine(msg.sender) {
|
|
| 468 |
✓ 17.2K
|
if (principalRepaid == 0 && interestRepaid == 0) {
|
|
| 469 |
revert TranchePool__InvalidTransferAmount(0); |
||
| 470 |
} |
||
| 471 | |||
| 472 |
/*////////////////////////////////////////////////////////////// |
||
| 473 |
INTEREST WATERFALL (INDEXED) |
||
| 474 |
//////////////////////////////////////////////////////////////*/ |
||
| 475 | |||
| 476 |
✓ 17.2K
|
uint256 remainingInterest = interestRepaid; |
|
| 477 |
✓ 17.2K
|
s_totalUnclaimedInterest += interestRepaid; |
|
| 478 | |||
| 479 |
// 1️⃣ Senior interest |
||
| 480 |
if ( |
||
| 481 |
✓ 17.2K
|
remainingInterest > 0 && |
|
| 482 |
✓ 17.1K
|
seniorAccruedInterest > 0 && |
|
| 483 |
✓ 10.9K
|
s_totalSeniorShares > 0 |
|
| 484 |
) {
|
||
| 485 |
uint256 seniorPaid = _minimum( |
||
| 486 |
remainingInterest, |
||
| 487 |
✓ 10.9K
|
seniorAccruedInterest |
|
| 488 |
); |
||
| 489 |
✓ 10.9K
|
seniorAccruedInterest -= seniorPaid; |
|
| 490 |
✓ 10.9K
|
seniorInterestIndex += (seniorPaid * 1e18) / s_totalSeniorShares; |
|
| 491 |
✓ 10.9K
|
remainingInterest -= seniorPaid; |
|
| 492 |
} |
||
| 493 | |||
| 494 |
// 2️⃣ Junior interest |
||
| 495 |
if ( |
||
| 496 |
✓ 17.2K
|
remainingInterest > 0 && |
|
| 497 |
✓ 15.5K
|
juniorAccruedInterest > 0 && |
|
| 498 |
✓ 12.6K
|
s_totalJuniorShares > 0 |
|
| 499 |
) {
|
||
| 500 |
uint256 juniorPaid = _minimum( |
||
| 501 |
remainingInterest, |
||
| 502 |
✓ 12.6K
|
juniorAccruedInterest |
|
| 503 |
); |
||
| 504 |
✓ 23.5K
|
juniorAccruedInterest -= juniorPaid; |
|
| 505 |
✓ 12.6K
|
juniorInterestIndex += (juniorPaid * 1e18) / s_totalJuniorShares; |
|
| 506 |
✓ 12.6K
|
remainingInterest -= juniorPaid; |
|
| 507 |
} |
||
| 508 | |||
| 509 |
// 3️⃣ Equity / overflow interest |
||
| 510 |
✓ 17.2K
|
if (remainingInterest > 0) {
|
|
| 511 |
✓ 12.3K
|
if (s_totalEquityShares > 0) {
|
|
| 512 |
equityInterestIndex += |
||
| 513 |
✓ 11.6K
|
(remainingInterest * 1e18) / |
|
| 514 |
s_totalEquityShares; |
||
| 515 |
equityAccruedInterest -= _minimum( |
||
| 516 |
✓ 11.6K
|
equityAccruedInterest, |
|
| 517 |
remainingInterest |
||
| 518 |
); |
||
| 519 |
✓ 619
|
} else if (s_totalJuniorShares > 0) {
|
|
| 520 |
// no equity → junior gets excess |
||
| 521 |
juniorInterestIndex += |
||
| 522 |
✓ 619
|
(remainingInterest * 1e18) / |
|
| 523 |
s_totalJuniorShares; |
||
| 524 |
} else {
|
||
| 525 |
// no LPs left → protocol revenue |
||
| 526 |
s_protocolRevenue += remainingInterest; |
||
| 527 |
} |
||
| 528 |
} |
||
| 529 | |||
| 530 |
/*////////////////////////////////////////////////////////////// |
||
| 531 |
PRINCIPAL REDEMPTION |
||
| 532 |
(REVERSE OF LOSS WATERFALL — NO RATIOS) |
||
| 533 |
//////////////////////////////////////////////////////////////*/ |
||
| 534 | |||
| 535 |
✓ 17.2K
|
if (principalRepaid > 0) {
|
|
| 536 |
✓ 16.3K
|
uint256 remaining = principalRepaid; |
|
| 537 | |||
| 538 |
// Senior first (restore safest capital) |
||
| 539 |
✓ 16.3K
|
if (remaining > 0 && s_seniorTrancheDeployedValue > 0) {
|
|
| 540 |
uint256 seniorPay = _minimum( |
||
| 541 |
remaining, |
||
| 542 |
✓ 10.2K
|
s_seniorTrancheDeployedValue |
|
| 543 |
); |
||
| 544 |
✓ 10.2K
|
s_seniorTrancheDeployedValue -= seniorPay; |
|
| 545 |
✓ 10.2K
|
s_seniorTrancheIdleValue += seniorPay; |
|
| 546 |
✓ 10.2K
|
remaining -= seniorPay; |
|
| 547 |
} |
||
| 548 | |||
| 549 |
// Junior next |
||
| 550 |
✓ 16.3K
|
if (remaining > 0 && s_juniorTrancheDeployedValue > 0) {
|
|
| 551 |
uint256 juniorPay = _minimum( |
||
| 552 |
remaining, |
||
| 553 |
✓ 8.6K
|
s_juniorTrancheDeployedValue |
|
| 554 |
); |
||
| 555 |
✓ 8.6K
|
s_juniorTrancheDeployedValue -= juniorPay; |
|
| 556 |
✓ 8.6K
|
s_juniorTrancheIdleValue += juniorPay; |
|
| 557 |
✓ 8.6K
|
remaining -= juniorPay; |
|
| 558 |
} |
||
| 559 | |||
| 560 |
// Equity last |
||
| 561 |
✓ 16.3K
|
if (remaining > 0 && s_equityTrancheDeployedValue > 0) {
|
|
| 562 |
uint256 equityPay = _minimum( |
||
| 563 |
remaining, |
||
| 564 |
✓ 7.7K
|
s_equityTrancheDeployedValue |
|
| 565 |
); |
||
| 566 |
✓ 26.4K
|
s_equityTrancheDeployedValue -= equityPay; |
|
| 567 |
✓ 7.7K
|
s_equityTrancheIdleValue += equityPay; |
|
| 568 |
✓ 7.7K
|
remaining -= equityPay; |
|
| 569 |
} |
||
| 570 | |||
| 571 |
// Safety: should never happen unless LoanEngine lies |
||
| 572 |
if (remaining > 0) {
|
||
| 573 |
revert TranchePool__PrincipalRepaymentExceeded(); |
||
| 574 |
} |
||
| 575 |
} |
||
| 576 |
} |
||
| 577 | |||
| 578 |
// lp profit withdrawal is pending but it can only be implemented |
||
| 579 |
// after the loan enginge implementation which determines how the |
||
| 580 |
// interest will be accured and the distribution is dependent on the |
||
| 581 |
// share capacity |
||
| 582 | |||
| 583 |
function onLoss( |
||
| 584 |
uint256 principalLoss, |
||
| 585 |
uint256 interestAccrued |
||
| 586 |
✓ 5
|
) external onlyLoanEngine(msg.sender) {
|
|
| 587 |
✓ 5
|
if (principalLoss == 0 && interestAccrued == 0) {
|
|
| 588 |
revert TranchePool__ZeroValueError(); |
||
| 589 |
} |
||
| 590 | |||
| 591 |
/*////////////////////////////////////////////////////////////// |
||
| 592 |
1️⃣ CANCEL GHOST INTEREST |
||
| 593 |
(SAME PRIORITY AS INTEREST PAYOUT) |
||
| 594 |
//////////////////////////////////////////////////////////////*/ |
||
| 595 | |||
| 596 |
✓ 5
|
uint256 remainingInterest = interestAccrued; |
|
| 597 | |||
| 598 |
// Cancel senior accrued interest first |
||
| 599 |
✓ 5
|
if (remainingInterest > 0 && seniorAccruedInterest > 0) {
|
|
| 600 |
uint256 seniorCancel = _minimum( |
||
| 601 |
remainingInterest, |
||
| 602 |
✓ 5
|
seniorAccruedInterest |
|
| 603 |
); |
||
| 604 |
✓ 5
|
seniorAccruedInterest -= seniorCancel; |
|
| 605 |
✓ 5
|
remainingInterest -= seniorCancel; |
|
| 606 |
} |
||
| 607 | |||
| 608 |
// Then junior |
||
| 609 |
✓ 5
|
if (remainingInterest > 0 && juniorAccruedInterest > 0) {
|
|
| 610 |
uint256 juniorCancel = _minimum( |
||
| 611 |
remainingInterest, |
||
| 612 |
✓ 17.1K
|
juniorAccruedInterest |
|
| 613 |
); |
||
| 614 |
✓ 17.1K
|
juniorAccruedInterest -= juniorCancel; |
|
| 615 |
✓ 17.1K
|
remainingInterest -= juniorCancel; |
|
| 616 |
} |
||
| 617 | |||
| 618 |
// Any remaining interest is ignored (equity / protocol had no promise) |
||
| 619 | |||
| 620 |
/*////////////////////////////////////////////////////////////// |
||
| 621 |
2️⃣ PRINCIPAL LOSS WATERFALL |
||
| 622 |
Equity → Junior → Senior |
||
| 623 |
//////////////////////////////////////////////////////////////*/ |
||
| 624 | |||
| 625 |
✓ 5
|
s_totalLoss += principalLoss; |
|
| 626 |
✓ 5
|
uint256 remaining = principalLoss; |
|
| 627 | |||
| 628 |
✓ 5
|
uint256 equityLoss; |
|
| 629 |
✓ 5
|
uint256 juniorLoss; |
|
| 630 |
✓ 5
|
uint256 seniorLoss; |
|
| 631 | |||
| 632 |
// Equity absorbs first |
||
| 633 |
✓ 5
|
if (remaining > 0 && s_equityTrancheDeployedValue > 0) {
|
|
| 634 |
✓ 5
|
equityLoss = _minimum(remaining, s_equityTrancheDeployedValue); |
|
| 635 |
✓ 5
|
s_equityTrancheDeployedValue -= equityLoss; |
|
| 636 |
✓ 5
|
equityPrincipalShortfall += equityLoss; |
|
| 637 |
✓ 5
|
remaining -= equityLoss; |
|
| 638 |
} |
||
| 639 | |||
| 640 |
// Junior next |
||
| 641 |
✓ 5
|
if (remaining > 0 && s_juniorTrancheDeployedValue > 0) {
|
|
| 642 |
juniorLoss = _minimum(remaining, s_juniorTrancheDeployedValue); |
||
| 643 |
s_juniorTrancheDeployedValue -= juniorLoss; |
||
| 644 |
juniorPrincipalShortfall += juniorLoss; |
||
| 645 |
remaining -= juniorLoss; |
||
| 646 |
} |
||
| 647 | |||
| 648 |
// Senior last |
||
| 649 |
✓ 5
|
if (remaining > 0 && s_seniorTrancheDeployedValue > 0) {
|
|
| 650 |
seniorLoss = _minimum(remaining, s_seniorTrancheDeployedValue); |
||
| 651 |
s_seniorTrancheDeployedValue -= seniorLoss; |
||
| 652 |
seniorPrincipalShortfall += seniorLoss; |
||
| 653 |
remaining -= seniorLoss; |
||
| 654 |
} |
||
| 655 | |||
| 656 |
✓ 5
|
if (remaining > 0) {
|
|
| 657 |
revert TranchePool__LossExceededCapital(remaining); |
||
| 658 |
} |
||
| 659 | |||
| 660 |
✓ 29.6K
|
emit LossAllocated(seniorLoss, juniorLoss, equityLoss); |
|
| 661 |
} |
||
| 662 | |||
| 663 |
// on recovery what happens is the protocol may recover more than he lost and it can cause appreciation of the share value when withdrawing, keeping the design simple because adding it to interest accured make no difference at the end of withdrawing. |
||
| 664 | |||
| 665 |
function onRecovery(uint256 amount) external onlyLoanEngine(msg.sender) {
|
||
| 666 |
if (amount == 0) {
|
||
| 667 |
revert TranchePool__ZeroValueError(); |
||
| 668 |
} |
||
| 669 | |||
| 670 |
s_totalRecovered += amount; |
||
| 671 |
uint256 remaining = amount; |
||
| 672 | |||
| 673 |
// Senior first |
||
| 674 |
if (remaining > 0 && seniorPrincipalShortfall > 0) {
|
||
| 675 |
uint256 seniorPay = _minimum(remaining, seniorPrincipalShortfall); |
||
| 676 |
seniorPrincipalShortfall -= seniorPay; |
||
| 677 |
s_seniorTrancheIdleValue += seniorPay; |
||
| 678 |
remaining -= seniorPay; |
||
| 679 |
} |
||
| 680 | |||
| 681 |
// Junior next |
||
| 682 |
if (remaining > 0 && juniorPrincipalShortfall > 0) {
|
||
| 683 |
uint256 juniorPay = _minimum(remaining, juniorPrincipalShortfall); |
||
| 684 |
juniorPrincipalShortfall -= juniorPay; |
||
| 685 |
s_juniorTrancheIdleValue += juniorPay; |
||
| 686 |
remaining -= juniorPay; |
||
| 687 |
} |
||
| 688 | |||
| 689 |
// Equity last |
||
| 690 |
if (remaining > 0 && equityPrincipalShortfall > 0) {
|
||
| 691 |
uint256 equityPay = _minimum(remaining, equityPrincipalShortfall); |
||
| 692 |
equityPrincipalShortfall -= equityPay; |
||
| 693 |
s_equityTrancheIdleValue += equityPay; |
||
| 694 |
remaining -= equityPay; |
||
| 695 |
} |
||
| 696 | |||
| 697 |
// Any excess is true upside → equity |
||
| 698 |
if (remaining > 0) {
|
||
| 699 |
s_equityTrancheIdleValue += remaining; |
||
| 700 |
} |
||
| 701 | |||
| 702 |
emit RecoverAmountTransferredToTranchePool(amount, block.timestamp); |
||
| 703 |
} |
||
| 704 | |||
| 705 |
// when the pool closes if the user withdraw the shares before claiming interest on those he will lose the interest for the withdrawn shares |
||
| 706 |
function claimSeniorInterest() external {
|
||
| 707 |
uint256 userShares = s_seniorTrancheShares[msg.sender]; |
||
| 708 |
if (userShares == 0) revert TranchePool__InsufficientShares(); |
||
| 709 | |||
| 710 |
uint256 indexDelta = seniorInterestIndex - seniorUserIndex[msg.sender]; |
||
| 711 | |||
| 712 |
if (indexDelta == 0) revert TranchePool__ZeroWithdrawal(); |
||
| 713 | |||
| 714 |
uint256 claimable = (userShares * indexDelta) / 1e18; |
||
| 715 | |||
| 716 |
// CHANGED: update user index BEFORE transfer |
||
| 717 |
seniorUserIndex[msg.sender] = seniorInterestIndex; |
||
| 718 |
s_totalUnclaimedInterest -= claimable; |
||
| 719 | |||
| 720 |
IERC20(s_stableCoin).safeTransfer(msg.sender, claimable); |
||
| 721 |
} |
||
| 722 | |||
| 723 |
function claimJuniorInterest() external {
|
||
| 724 |
uint256 userShares = s_juniorTrancheShares[msg.sender]; |
||
| 725 |
if (userShares == 0) revert TranchePool__InsufficientShares(); |
||
| 726 | |||
| 727 |
uint256 indexDelta = juniorInterestIndex - juniorUserIndex[msg.sender]; |
||
| 728 | |||
| 729 |
if (indexDelta == 0) revert TranchePool__ZeroWithdrawal(); |
||
| 730 | |||
| 731 |
uint256 claimable = (userShares * indexDelta) / 1e18; |
||
| 732 | |||
| 733 |
// CHANGED: update user index BEFORE transfer |
||
| 734 |
juniorUserIndex[msg.sender] = juniorInterestIndex; |
||
| 735 |
s_totalUnclaimedInterest -= claimable; |
||
| 736 | |||
| 737 |
IERC20(s_stableCoin).safeTransfer(msg.sender, claimable); |
||
| 738 |
} |
||
| 739 | |||
| 740 |
function claimEquityInterest() |
||
| 741 |
external |
||
| 742 |
isWhiteListedForEquityTranche(msg.sender) |
||
| 743 |
{
|
||
| 744 |
uint256 userShares = s_equityTrancheShares[msg.sender]; |
||
| 745 |
if (userShares == 0) revert TranchePool__InsufficientShares(); |
||
| 746 | |||
| 747 |
uint256 indexDelta = equityInterestIndex - equityUserIndex[msg.sender]; |
||
| 748 | |||
| 749 |
if (indexDelta == 0) revert TranchePool__ZeroWithdrawal(); |
||
| 750 | |||
| 751 |
uint256 claimable = (userShares * indexDelta) / 1e18; |
||
| 752 |
s_totalUnclaimedInterest -= claimable; |
||
| 753 | |||
| 754 |
// CHANGED: update user index BEFORE transfer |
||
| 755 |
equityUserIndex[msg.sender] = equityInterestIndex; |
||
| 756 | |||
| 757 |
✓ 17.2K
|
IERC20(s_stableCoin).safeTransfer(msg.sender, claimable); |
|
| 758 |
} |
||
| 759 | |||
| 760 |
/** |
||
| 761 |
* |
||
| 762 |
* |
||
| 763 |
* @notice Withdraw from senior tranche by burning shares |
||
| 764 |
* @param shares Number of shares to burn (0 = withdraw all) |
||
| 765 |
* passing zero amount will cause the burn of all shares |
||
| 766 |
*/ |
||
| 767 |
function withdrawSeniorTranche( |
||
| 768 |
uint256 shares |
||
| 769 |
) external isWhiteListed(msg.sender) {
|
||
| 770 |
if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
|
||
| 771 |
revert TranchePool__WithdrawNotAllowed(poolState); |
||
| 772 |
} |
||
| 773 |
uint256 userShares = s_seniorTrancheShares[msg.sender]; |
||
| 774 | |||
| 775 |
if (userShares == 0) {
|
||
| 776 |
revert TranchePool__InsufficientShares(); |
||
| 777 |
} |
||
| 778 | |||
| 779 |
// If shares is 0, withdraw everything |
||
| 780 |
uint256 sharesToBurn = shares == 0 ? userShares : shares; |
||
| 781 | |||
| 782 |
if (sharesToBurn > userShares) {
|
||
| 783 |
revert TranchePool__InsufficientShares(); |
||
| 784 |
} |
||
| 785 | |||
| 786 |
// Calculate amount to withdraw based on current pool value |
||
| 787 |
uint256 amountToWithdraw = (sharesToBurn * s_seniorTrancheIdleValue) / |
||
| 788 |
s_totalSeniorShares; |
||
| 789 | |||
| 790 |
if (seniorUserIndex[msg.sender] != seniorInterestIndex) {
|
||
| 791 |
revert TranchePool__InterestNotClaimed(); |
||
| 792 |
} |
||
| 793 | |||
| 794 |
if (amountToWithdraw == 0) {
|
||
| 795 |
revert TranchePool__ZeroWithdrawal(); |
||
| 796 |
} |
||
| 797 | |||
| 798 |
if (amountToWithdraw > s_seniorTrancheIdleValue) {
|
||
| 799 |
revert TranchePool__InsufficientLiquidity(); |
||
| 800 |
} |
||
| 801 | |||
| 802 |
// Update state before transfer (CEI pattern) |
||
| 803 |
s_seniorTrancheShares[msg.sender] -= sharesToBurn; |
||
| 804 |
s_totalSeniorShares -= sharesToBurn; |
||
| 805 |
s_seniorTrancheIdleValue -= amountToWithdraw; |
||
| 806 |
s_totalDeposited -= amountToWithdraw; |
||
| 807 |
// Transfer tokens |
||
| 808 |
IERC20(s_stableCoin).safeTransfer(msg.sender, amountToWithdraw); |
||
| 809 | |||
| 810 |
emit WithdrawnFromSeniorTranche( |
||
| 811 |
msg.sender, |
||
| 812 |
amountToWithdraw, |
||
| 813 |
sharesToBurn, |
||
| 814 |
block.timestamp |
||
| 815 |
); |
||
| 816 |
} |
||
| 817 | |||
| 818 |
/** |
||
| 819 |
* @notice Withdraw from junior tranche by burning shares |
||
| 820 |
* @param shares Number of shares to burn (0 = withdraw all) |
||
| 821 |
*/ |
||
| 822 |
function withdrawJuniorTranche( |
||
| 823 |
uint256 shares |
||
| 824 |
) external isWhiteListed(msg.sender) {
|
||
| 825 |
if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
|
||
| 826 |
revert TranchePool__WithdrawNotAllowed(poolState); |
||
| 827 |
} |
||
| 828 |
uint256 userShares = s_juniorTrancheShares[msg.sender]; |
||
| 829 | |||
| 830 |
if (userShares == 0) {
|
||
| 831 |
revert TranchePool__InsufficientShares(); |
||
| 832 |
} |
||
| 833 | |||
| 834 |
if (juniorUserIndex[msg.sender] != juniorInterestIndex) {
|
||
| 835 |
revert TranchePool__InterestNotClaimed(); |
||
| 836 |
} |
||
| 837 | |||
| 838 |
uint256 sharesToBurn = shares == 0 ? userShares : shares; |
||
| 839 | |||
| 840 |
if (sharesToBurn > userShares) {
|
||
| 841 |
revert TranchePool__InsufficientShares(); |
||
| 842 |
} |
||
| 843 | |||
| 844 |
uint256 amountToWithdraw = (sharesToBurn * s_juniorTrancheIdleValue) / |
||
| 845 |
s_totalJuniorShares; |
||
| 846 | |||
| 847 |
if (amountToWithdraw == 0) {
|
||
| 848 |
revert TranchePool__ZeroWithdrawal(); |
||
| 849 |
} |
||
| 850 |
if (amountToWithdraw > s_juniorTrancheIdleValue) {
|
||
| 851 |
revert TranchePool__InsufficientLiquidity(); |
||
| 852 |
} |
||
| 853 | |||
| 854 |
s_juniorTrancheShares[msg.sender] -= sharesToBurn; |
||
| 855 |
s_totalJuniorShares -= sharesToBurn; |
||
| 856 |
s_juniorTrancheIdleValue -= amountToWithdraw; |
||
| 857 |
s_totalDeposited -= amountToWithdraw; |
||
| 858 | |||
| 859 |
IERC20(s_stableCoin).safeTransfer(msg.sender, amountToWithdraw); |
||
| 860 | |||
| 861 |
emit WithdrawnFromJuniorTranche( |
||
| 862 |
msg.sender, |
||
| 863 |
amountToWithdraw, |
||
| 864 |
sharesToBurn, |
||
| 865 |
block.timestamp |
||
| 866 |
); |
||
| 867 |
} |
||
| 868 | |||
| 869 |
function withdrawEquityTranche( |
||
| 870 |
uint256 shares |
||
| 871 |
) external isWhiteListedForEquityTranche(msg.sender) {
|
||
| 872 |
if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
|
||
| 873 |
revert TranchePool__WithdrawNotAllowed(poolState); |
||
| 874 |
} |
||
| 875 |
uint256 userShares = s_equityTrancheShares[msg.sender]; |
||
| 876 | |||
| 877 |
if (userShares == 0) {
|
||
| 878 |
revert TranchePool__InsufficientShares(); |
||
| 879 |
} |
||
| 880 |
if (equityUserIndex[msg.sender] != equityInterestIndex) {
|
||
| 881 |
revert TranchePool__InterestNotClaimed(); |
||
| 882 |
} |
||
| 883 |
uint256 sharesToBurn = shares == 0 ? userShares : shares; |
||
| 884 |
if (sharesToBurn > userShares) {
|
||
| 885 |
revert TranchePool__InsufficientShares(); |
||
| 886 |
} |
||
| 887 | |||
| 888 |
uint256 amountToWithdraw = (sharesToBurn * s_equityTrancheIdleValue) / |
||
| 889 |
s_totalEquityShares; |
||
| 890 | |||
| 891 |
if (amountToWithdraw == 0) {
|
||
| 892 |
revert TranchePool__ZeroWithdrawal(); |
||
| 893 |
} |
||
| 894 |
if (amountToWithdraw > s_equityTrancheIdleValue) {
|
||
| 895 |
revert TranchePool__InsufficientLiquidity(); |
||
| 896 |
} |
||
| 897 |
s_equityTrancheShares[msg.sender] -= sharesToBurn; |
||
| 898 |
s_totalEquityShares -= sharesToBurn; |
||
| 899 |
s_equityTrancheIdleValue -= amountToWithdraw; |
||
| 900 |
s_totalDeposited -= amountToWithdraw; |
||
| 901 | |||
| 902 |
IERC20(s_stableCoin).safeTransfer(msg.sender, amountToWithdraw); |
||
| 903 | |||
| 904 |
emit WithdrawnFromEquityTranche( |
||
| 905 |
msg.sender, |
||
| 906 |
amountToWithdraw, |
||
| 907 |
sharesToBurn, |
||
| 908 |
block.timestamp |
||
| 909 |
); |
||
| 910 |
} |
||
| 911 | |||
| 912 |
/** |
||
| 913 |
* @notice Withdraw specific amount from senior tranche |
||
| 914 |
* @param amount Amount of tokens to withdraw |
||
| 915 |
*/ |
||
| 916 |
function withdrawSeniorTrancheByAmount( |
||
| 917 |
uint256 amount |
||
| 918 |
) external isWhiteListed(msg.sender) {
|
||
| 919 |
if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
|
||
| 920 |
revert TranchePool__WithdrawNotAllowed(poolState); |
||
| 921 |
} |
||
| 922 |
if (amount == 0) {
|
||
| 923 |
revert TranchePool__ZeroWithdrawal(); |
||
| 924 |
} |
||
| 925 |
if (seniorUserIndex[msg.sender] != seniorInterestIndex) {
|
||
| 926 |
revert TranchePool__InterestNotClaimed(); |
||
| 927 |
} |
||
| 928 | |||
| 929 |
uint256 userBalance = getSeniorTrancheBalance(msg.sender); |
||
| 930 | |||
| 931 |
if (amount > userBalance) {
|
||
| 932 |
revert TranchePool__InsufficientShares(); |
||
| 933 |
} |
||
| 934 | |||
| 935 |
if (amount > s_seniorTrancheIdleValue) {
|
||
| 936 |
revert TranchePool__InsufficientLiquidity(); |
||
| 937 |
} |
||
| 938 | |||
| 939 |
// Calculate shares to burn for this amount |
||
| 940 |
uint256 sharesToBurn = (amount * s_totalSeniorShares) / |
||
| 941 |
(s_seniorTrancheIdleValue); |
||
| 942 | |||
| 943 |
// Handle rounding - ensure we don't try to withdraw more than available |
||
| 944 |
if (sharesToBurn > s_seniorTrancheShares[msg.sender]) {
|
||
| 945 |
sharesToBurn = s_seniorTrancheShares[msg.sender]; |
||
| 946 |
} |
||
| 947 | |||
| 948 |
s_seniorTrancheShares[msg.sender] -= sharesToBurn; |
||
| 949 |
s_totalSeniorShares -= sharesToBurn; |
||
| 950 |
s_seniorTrancheIdleValue -= amount; |
||
| 951 |
s_totalDeposited -= amount; |
||
| 952 | |||
| 953 |
IERC20(s_stableCoin).safeTransfer(msg.sender, amount); |
||
| 954 | |||
| 955 |
emit WithdrawnFromSeniorTranche( |
||
| 956 |
msg.sender, |
||
| 957 |
amount, |
||
| 958 |
sharesToBurn, |
||
| 959 |
block.timestamp |
||
| 960 |
); |
||
| 961 |
} |
||
| 962 | |||
| 963 |
/** |
||
| 964 |
* @notice Withdraw specific amount from junior tranche |
||
| 965 |
* @param amount Amount of tokens to withdraw |
||
| 966 |
*/ |
||
| 967 |
function withdrawJuniorTrancheByAmount( |
||
| 968 |
uint256 amount |
||
| 969 |
) external isWhiteListed(msg.sender) {
|
||
| 970 |
if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
|
||
| 971 |
revert TranchePool__WithdrawNotAllowed(poolState); |
||
| 972 |
} |
||
| 973 |
if (amount == 0) {
|
||
| 974 |
revert TranchePool__ZeroWithdrawal(); |
||
| 975 |
} |
||
| 976 | |||
| 977 |
if (juniorUserIndex[msg.sender] != juniorInterestIndex) {
|
||
| 978 |
revert TranchePool__InterestNotClaimed(); |
||
| 979 |
} |
||
| 980 | |||
| 981 |
uint256 userBalance = getJuniorTrancheBalance(msg.sender); |
||
| 982 | |||
| 983 |
if (amount > userBalance) {
|
||
| 984 |
revert TranchePool__InsufficientShares(); |
||
| 985 |
} |
||
| 986 | |||
| 987 |
if (amount > s_juniorTrancheIdleValue) {
|
||
| 988 |
revert TranchePool__InsufficientLiquidity(); |
||
| 989 |
} |
||
| 990 | |||
| 991 |
uint256 sharesToBurn = (amount * s_totalJuniorShares) / |
||
| 992 |
(s_juniorTrancheIdleValue); |
||
| 993 | |||
| 994 |
if (sharesToBurn > s_juniorTrancheShares[msg.sender]) {
|
||
| 995 |
sharesToBurn = s_juniorTrancheShares[msg.sender]; |
||
| 996 |
} |
||
| 997 | |||
| 998 |
s_juniorTrancheShares[msg.sender] -= sharesToBurn; |
||
| 999 |
s_totalJuniorShares -= sharesToBurn; |
||
| 1000 |
s_juniorTrancheIdleValue -= amount; |
||
| 1001 |
s_totalDeposited -= amount; |
||
| 1002 | |||
| 1003 |
IERC20(s_stableCoin).safeTransfer(msg.sender, amount); |
||
| 1004 | |||
| 1005 |
emit WithdrawnFromJuniorTranche( |
||
| 1006 |
msg.sender, |
||
| 1007 |
amount, |
||
| 1008 |
sharesToBurn, |
||
| 1009 |
block.timestamp |
||
| 1010 |
); |
||
| 1011 |
} |
||
| 1012 | |||
| 1013 |
function withdrawEquityTrancheByAmount( |
||
| 1014 |
uint256 amount |
||
| 1015 |
) external isWhiteListedForEquityTranche(msg.sender) {
|
||
| 1016 |
if (poolState != PoolState.OPEN && poolState != PoolState.CLOSED) {
|
||
| 1017 |
revert TranchePool__WithdrawNotAllowed(poolState); |
||
| 1018 |
} |
||
| 1019 |
if (amount == 0) {
|
||
| 1020 |
revert TranchePool__ZeroWithdrawal(); |
||
| 1021 |
} |
||
| 1022 | |||
| 1023 |
if (equityUserIndex[msg.sender] != equityInterestIndex) {
|
||
| 1024 |
revert TranchePool__InterestNotClaimed(); |
||
| 1025 |
} |
||
| 1026 | |||
| 1027 |
uint256 userBalance = getEquityTrancheBalance(msg.sender); |
||
| 1028 | |||
| 1029 |
if (amount > userBalance) {
|
||
| 1030 |
revert TranchePool__InsufficientShares(); |
||
| 1031 |
} |
||
| 1032 | |||
| 1033 |
if (amount > s_equityTrancheIdleValue) {
|
||
| 1034 |
revert TranchePool__InsufficientLiquidity(); |
||
| 1035 |
} |
||
| 1036 | |||
| 1037 |
uint256 sharesToBurn = (amount * s_totalEquityShares) / |
||
| 1038 |
(s_equityTrancheIdleValue); |
||
| 1039 | |||
| 1040 |
if (sharesToBurn > s_equityTrancheShares[msg.sender]) {
|
||
| 1041 |
sharesToBurn = s_equityTrancheShares[msg.sender]; |
||
| 1042 |
} |
||
| 1043 | |||
| 1044 |
s_equityTrancheShares[msg.sender] -= sharesToBurn; |
||
| 1045 |
s_totalEquityShares -= sharesToBurn; |
||
| 1046 |
s_equityTrancheIdleValue -= amount; |
||
| 1047 |
s_totalDeposited -= amount; |
||
| 1048 | |||
| 1049 |
IERC20(s_stableCoin).safeTransfer(msg.sender, amount); |
||
| 1050 | |||
| 1051 |
emit WithdrawnFromEquityTranche( |
||
| 1052 |
msg.sender, |
||
| 1053 |
amount, |
||
| 1054 |
sharesToBurn, |
||
| 1055 |
block.timestamp |
||
| 1056 |
); |
||
| 1057 |
} |
||
| 1058 | |||
| 1059 |
/** |
||
| 1060 |
* @notice Get the current balance of a user in the senior tranche |
||
| 1061 |
*/ |
||
| 1062 |
function getSeniorTrancheBalance( |
||
| 1063 |
address user |
||
| 1064 |
) public view returns (uint256) {
|
||
| 1065 |
if (s_totalSeniorShares == 0) return 0; |
||
| 1066 |
return |
||
| 1067 |
(s_seniorTrancheShares[user] * s_seniorTrancheIdleValue) / |
||
| 1068 |
s_totalSeniorShares; |
||
| 1069 |
} |
||
| 1070 | |||
| 1071 |
/** |
||
| 1072 |
* @notice Get the current balance of a user in the junior tranche |
||
| 1073 |
*/ |
||
| 1074 |
function getJuniorTrancheBalance( |
||
| 1075 |
address user |
||
| 1076 |
) public view returns (uint256) {
|
||
| 1077 |
if (s_totalJuniorShares == 0) return 0; |
||
| 1078 |
return |
||
| 1079 |
(s_juniorTrancheShares[user] * s_juniorTrancheIdleValue) / |
||
| 1080 |
s_totalJuniorShares; |
||
| 1081 |
} |
||
| 1082 | |||
| 1083 |
function getEquityTrancheBalance( |
||
| 1084 |
address user |
||
| 1085 |
) public view returns (uint256) {
|
||
| 1086 |
if (s_totalEquityShares == 0) return 0; |
||
| 1087 |
return |
||
| 1088 |
(s_equityTrancheShares[user] * s_equityTrancheIdleValue) / |
||
| 1089 |
s_totalEquityShares; |
||
| 1090 |
} |
||
| 1091 | |||
| 1092 |
// Admin functions |
||
| 1093 |
function setMinimumDepositAmountJuniorTranche( |
||
| 1094 |
uint256 amount |
||
| 1095 |
) external onlyOwner {
|
||
| 1096 |
✓ 1
|
if (amount > s_juniorTrancheMaxCap) {
|
|
| 1097 |
revert TranchePool__InvalidMinDepositAmount(); |
||
| 1098 |
} |
||
| 1099 |
✓ 1
|
s_minimumDepositAmountJuniorTranche = amount; |
|
| 1100 |
} |
||
| 1101 | |||
| 1102 |
function setMinimumDepositAmountSeniorTranche( |
||
| 1103 |
uint256 amount |
||
| 1104 |
) external onlyOwner {
|
||
| 1105 |
✓ 1
|
if (amount > s_seniorTrancheMaxCap) {
|
|
| 1106 |
revert TranchePool__InvalidMinDepositAmount(); |
||
| 1107 |
} |
||
| 1108 |
✓ 1
|
s_minimumDepositAmountSeniorTranche = amount; |
|
| 1109 |
} |
||
| 1110 | |||
| 1111 |
function setMinimumDepositAmountEquityTranche( |
||
| 1112 |
uint256 amount |
||
| 1113 |
) external onlyOwner {
|
||
| 1114 |
✓ 1
|
if (amount > s_equityTrancheMaxCap) {
|
|
| 1115 |
revert TranchePool__InvalidMinDepositAmount(); |
||
| 1116 |
} |
||
| 1117 |
✓ 1
|
s_minimumDepositAmountEquityTranche = amount; |
|
| 1118 |
} |
||
| 1119 | |||
| 1120 |
function setTrancheCapitalAllocationFactorSenior( |
||
| 1121 |
uint256 factor |
||
| 1122 |
) external onlyOwner {
|
||
| 1123 |
✓ 1
|
if (factor + s_capital_allocation_factor_junior > 100) |
|
| 1124 |
revert TranchePool__InvalidAllocationRatio(); |
||
| 1125 |
✓ 1
|
s_capital_allocation_factor_senior = factor; |
|
| 1126 |
✓ 1
|
emit CapitalAllocationFactorUpdatedSenior(factor); |
|
| 1127 |
} |
||
| 1128 | |||
| 1129 |
function setTrancheCapitalAllocationFactorJunior( |
||
| 1130 |
uint256 factor |
||
| 1131 |
) external onlyOwner {
|
||
| 1132 |
✓ 1
|
if (factor + s_capital_allocation_factor_senior > 100) |
|
| 1133 |
revert TranchePool__InvalidAllocationRatio(); |
||
| 1134 |
✓ 1
|
s_capital_allocation_factor_junior = factor; |
|
| 1135 |
✓ 1
|
emit CapitalAllocationFactorUpdatedJunior(factor); |
|
| 1136 |
} |
||
| 1137 | |||
| 1138 |
function setSeniorAPR(uint256 apr) external onlyOwner {
|
||
| 1139 |
✓ 1
|
if (apr == 0) {
|
|
| 1140 |
revert TranchePool__ZeroAPRError(); |
||
| 1141 |
} |
||
| 1142 |
✓ 1
|
s_senior_apr = apr; |
|
| 1143 |
} |
||
| 1144 | |||
| 1145 |
function setTargetJuniorAPR(uint256 apr) external onlyOwner {
|
||
| 1146 |
✓ 1
|
if (apr == 0) {
|
|
| 1147 |
revert TranchePool__ZeroAPRError(); |
||
| 1148 |
} |
||
| 1149 |
✓ 1
|
s_target_junior_apr = apr; |
|
| 1150 |
} |
||
| 1151 | |||
| 1152 |
function setPoolState(PoolState newState) external onlyOwner {
|
||
| 1153 |
✓ 29.6K
|
if (uint256(newState) < uint256(poolState)) {
|
|
| 1154 |
revert TranchePool__InvalidStateTransition(newState); |
||
| 1155 |
} |
||
| 1156 | |||
| 1157 |
✓ 29.6K
|
if (newState == PoolState.CLOSED) {
|
|
| 1158 |
✓ 3.1K
|
if (getTotalDeployedValue() > 0) {
|
|
| 1159 |
revert TranchePool__DeployedCapitalExists(); |
||
| 1160 |
} |
||
| 1161 |
} |
||
| 1162 | |||
| 1163 |
poolState = newState; |
||
| 1164 | |||
| 1165 |
✓ 29.6K
|
emit PoolStateUpdated(newState); |
|
| 1166 |
} |
||
| 1167 | |||
| 1168 |
function setLoanEngine(address _loanEngine) external onlyOwner {
|
||
| 1169 |
✓ 1
|
if (_loanEngine == address(0)) {
|
|
| 1170 |
revert TranchePool__ZeroAddressError(); |
||
| 1171 |
} |
||
| 1172 |
✓ 1
|
loanEngine = _loanEngine; |
|
| 1173 |
} |
||
| 1174 | |||
| 1175 |
function setMaxAllocationCapSeniorTranche( |
||
| 1176 |
uint256 amount |
||
| 1177 |
) external onlyOwner {
|
||
| 1178 |
✓ 1
|
if (amount == 0) {
|
|
| 1179 |
revert TranchePool__ZeroValueError(); |
||
| 1180 |
} |
||
| 1181 |
✓ 1
|
if (amount < s_minimumDepositAmountSeniorTranche) {
|
|
| 1182 |
revert TranchePool__InvalidMaxCapAmount(); |
||
| 1183 |
} |
||
| 1184 |
✓ 1
|
s_seniorTrancheMaxCap = amount; |
|
| 1185 |
} |
||
| 1186 | |||
| 1187 |
function setMaxAllocationCapJuniorTranche( |
||
| 1188 |
uint256 amount |
||
| 1189 |
) external onlyOwner {
|
||
| 1190 |
✓ 1
|
if (amount == 0) {
|
|
| 1191 |
revert TranchePool__ZeroValueError(); |
||
| 1192 |
} |
||
| 1193 |
✓ 1
|
if (amount < s_minimumDepositAmountJuniorTranche) {
|
|
| 1194 |
revert TranchePool__InvalidMaxCapAmount(); |
||
| 1195 |
} |
||
| 1196 |
✓ 1
|
s_juniorTrancheMaxCap = amount; |
|
| 1197 |
} |
||
| 1198 | |||
| 1199 |
function setMaxAllocationCapEquityTranche( |
||
| 1200 |
uint256 amount |
||
| 1201 |
) external onlyOwner {
|
||
| 1202 |
✓ 1
|
if (amount == 0) {
|
|
| 1203 |
revert TranchePool__ZeroValueError(); |
||
| 1204 |
} |
||
| 1205 |
✓ 1
|
if (amount < s_minimumDepositAmountEquityTranche) {
|
|
| 1206 |
revert TranchePool__InvalidMaxCapAmount(); |
||
| 1207 |
} |
||
| 1208 |
✓ 1
|
s_equityTrancheMaxCap = amount; |
|
| 1209 |
} |
||
| 1210 | |||
| 1211 |
function updateWhitelist(address user, bool status) external onlyOwner {
|
||
| 1212 |
✓ 108
|
whiteListedLps[user] = status; |
|
| 1213 |
} |
||
| 1214 | |||
| 1215 |
function updateEquityTrancheWhiteList( |
||
| 1216 |
address user, |
||
| 1217 |
bool status |
||
| 1218 |
) external onlyOwner {
|
||
| 1219 |
✓ 4
|
whiteListedForEquityTranche[user] = status; |
|
| 1220 |
} |
||
| 1221 | |||
| 1222 |
function _minimum(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 1223 |
✓ 186.1K
|
if (a > b) {
|
|
| 1224 |
✓ 59.9K
|
return b; |
|
| 1225 |
} else {
|
||
| 1226 |
✓ 126.2K
|
return a; |
|
| 1227 |
} |
||
| 1228 |
} |
||
| 1229 | |||
| 1230 |
// getters |
||
| 1231 | |||
| 1232 |
function getTotalUnclaimedInterest() external view returns (uint256) {
|
||
| 1233 |
return s_totalUnclaimedInterest; |
||
| 1234 |
} |
||
| 1235 | |||
| 1236 |
function getSeniorTrancheMaxDepositCap() external view returns (uint256) {
|
||
| 1237 |
return s_seniorTrancheMaxCap; |
||
| 1238 |
} |
||
| 1239 | |||
| 1240 |
function getSeniorTrancheMinimumDepositAmount() |
||
| 1241 |
external |
||
| 1242 |
view |
||
| 1243 |
returns (uint256) |
||
| 1244 |
{
|
||
| 1245 |
return s_minimumDepositAmountSeniorTranche; |
||
| 1246 |
} |
||
| 1247 | |||
| 1248 |
function getSeniorTrancheShares( |
||
| 1249 |
address user |
||
| 1250 |
) external view returns (uint256) {
|
||
| 1251 |
return s_seniorTrancheShares[user]; |
||
| 1252 |
} |
||
| 1253 | |||
| 1254 |
function getTotalSeniorShares() external view returns (uint256) {
|
||
| 1255 |
return s_totalSeniorShares; |
||
| 1256 |
} |
||
| 1257 | |||
| 1258 |
function getSeniorTrancheIdleValue() external view returns (uint256) {
|
||
| 1259 |
✓ 35.8K
|
return s_seniorTrancheIdleValue; |
|
| 1260 |
} |
||
| 1261 | |||
| 1262 |
function getSeniorTrancheDeployedValue() external view returns (uint256) {
|
||
| 1263 |
return s_seniorTrancheDeployedValue; |
||
| 1264 |
} |
||
| 1265 | |||
| 1266 |
function getSeniorInterestIndex() external view returns (uint256) {
|
||
| 1267 |
return seniorInterestIndex; |
||
| 1268 |
} |
||
| 1269 | |||
| 1270 |
function getSeniorUserIndex(address user) external view returns (uint256) {
|
||
| 1271 |
return seniorUserIndex[user]; |
||
| 1272 |
} |
||
| 1273 | |||
| 1274 |
function getJuniorTrancheMaxDepositCap() external view returns (uint256) {
|
||
| 1275 |
return s_juniorTrancheMaxCap; |
||
| 1276 |
} |
||
| 1277 | |||
| 1278 |
function getJuniorInterestIndex() external view returns (uint256) {
|
||
| 1279 |
return juniorInterestIndex; |
||
| 1280 |
} |
||
| 1281 | |||
| 1282 |
function getJuniorTrancheMinimumDepositAmount() |
||
| 1283 |
external |
||
| 1284 |
view |
||
| 1285 |
returns (uint256) |
||
| 1286 |
{
|
||
| 1287 |
✓ 54.0K
|
return s_minimumDepositAmountJuniorTranche; |
|
| 1288 |
} |
||
| 1289 | |||
| 1290 |
function getJuniorTrancheShares( |
||
| 1291 |
address user |
||
| 1292 |
) external view returns (uint256) {
|
||
| 1293 |
return s_juniorTrancheShares[user]; |
||
| 1294 |
} |
||
| 1295 | |||
| 1296 |
function getTotalJuniorShares() external view returns (uint256) {
|
||
| 1297 |
return s_totalJuniorShares; |
||
| 1298 |
} |
||
| 1299 | |||
| 1300 |
function getJuniorTrancheIdleValue() external view returns (uint256) {
|
||
| 1301 |
✓ 31.7K
|
return s_juniorTrancheIdleValue; |
|
| 1302 |
} |
||
| 1303 | |||
| 1304 |
function getJuniorTrancheDeployedValue() external view returns (uint256) {
|
||
| 1305 |
✓ 31.7K
|
return s_juniorTrancheDeployedValue; |
|
| 1306 |
} |
||
| 1307 | |||
| 1308 |
function getJuniorUserIndex(address user) external view returns (uint256) {
|
||
| 1309 |
return juniorUserIndex[user]; |
||
| 1310 |
} |
||
| 1311 | |||
| 1312 |
function getEquityTrancheMaxDepositCap() external view returns (uint256) {
|
||
| 1313 |
✓ 108.3K
|
return s_equityTrancheMaxCap; |
|
| 1314 |
} |
||
| 1315 | |||
| 1316 |
function getEquityTrancheShares( |
||
| 1317 |
address user |
||
| 1318 |
) external view returns (uint256) {
|
||
| 1319 |
return s_equityTrancheShares[user]; |
||
| 1320 |
} |
||
| 1321 | |||
| 1322 |
function getTotalEquityShares() external view returns (uint256) {
|
||
| 1323 |
return s_totalEquityShares; |
||
| 1324 |
} |
||
| 1325 | |||
| 1326 |
function getEquityTrancheMinimumDepositAmount() |
||
| 1327 |
external |
||
| 1328 |
view |
||
| 1329 |
returns (uint256) |
||
| 1330 |
{
|
||
| 1331 |
✓ 65.3K
|
return s_minimumDepositAmountEquityTranche; |
|
| 1332 |
} |
||
| 1333 | |||
| 1334 |
function getEquityTrancheIdleValue() external view returns (uint256) {
|
||
| 1335 |
✓ 43.0K
|
return s_equityTrancheIdleValue; |
|
| 1336 |
} |
||
| 1337 | |||
| 1338 |
function getEquityTrancheDeployedValue() external view returns (uint256) {
|
||
| 1339 |
✓ 43.0K
|
return s_equityTrancheDeployedValue; |
|
| 1340 |
} |
||
| 1341 | |||
| 1342 |
function getEquityInterestIndex() external view returns (uint256) {
|
||
| 1343 |
return equityInterestIndex; |
||
| 1344 |
} |
||
| 1345 | |||
| 1346 |
function getEquityUserIndex(address user) external view returns (uint256) {
|
||
| 1347 |
return equityUserIndex[user]; |
||
| 1348 |
} |
||
| 1349 | |||
| 1350 |
function getPoolState() external view returns (PoolState) {
|
||
| 1351 |
return poolState; |
||
| 1352 |
} |
||
| 1353 | |||
| 1354 |
function getTotalDeployedValue() public view returns (uint256) {
|
||
| 1355 |
return |
||
| 1356 |
✓ 104.5K
|
s_seniorTrancheDeployedValue + |
|
| 1357 |
✓ 104.5K
|
s_juniorTrancheDeployedValue + |
|
| 1358 |
✓ 104.5K
|
s_equityTrancheDeployedValue; |
|
| 1359 |
} |
||
| 1360 | |||
| 1361 |
function getTotalIdleValue() external view returns (uint256) {
|
||
| 1362 |
return |
||
| 1363 |
✓ 610.6K
|
s_seniorTrancheIdleValue + |
|
| 1364 |
✓ 610.6K
|
s_juniorTrancheIdleValue + |
|
| 1365 |
✓ 610.6K
|
s_equityTrancheIdleValue; |
|
| 1366 |
} |
||
| 1367 | |||
| 1368 |
function getSeniorAllocationRatio() external view returns (uint256) {
|
||
| 1369 |
return s_capital_allocation_factor_senior; |
||
| 1370 |
} |
||
| 1371 | |||
| 1372 |
function getJuniorAllocationRatio() external view returns (uint256) {
|
||
| 1373 |
return s_capital_allocation_factor_junior; |
||
| 1374 |
} |
||
| 1375 | |||
| 1376 |
function getTotalDeposited() external view returns (uint256) {
|
||
| 1377 |
return s_totalDeposited; |
||
| 1378 |
} |
||
| 1379 | |||
| 1380 |
function getTotalLoss() external view returns (uint256) {
|
||
| 1381 |
return s_totalLoss; |
||
| 1382 |
} |
||
| 1383 | |||
| 1384 |
function getTotalRecovered() external view returns (uint256) {
|
||
| 1385 |
return s_totalRecovered; |
||
| 1386 |
} |
||
| 1387 | |||
| 1388 |
function getProtocolRevenue() external view returns (uint256) {
|
||
| 1389 |
return s_protocolRevenue; |
||
| 1390 |
} |
||
| 1391 | |||
| 1392 |
function getSeniorPrincipalShortfall() external view returns (uint256) {
|
||
| 1393 |
return seniorPrincipalShortfall; |
||
| 1394 |
} |
||
| 1395 | |||
| 1396 |
function getJuniorPrincipalShortfall() external view returns (uint256) {
|
||
| 1397 |
return juniorPrincipalShortfall; |
||
| 1398 |
} |
||
| 1399 | |||
| 1400 |
function getEquityPrincipalShortfall() external view returns (uint256) {
|
||
| 1401 |
return equityPrincipalShortfall; |
||
| 1402 |
} |
||
| 1403 |
} |
||
| 1404 |
0.0%
src/interfaces/ICreditPolicy.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 |
import {CreditPolicy} from "../CreditPolicy.sol";
|
||
| 4 | |||
| 5 |
interface ICreditPolicy {
|
||
| 6 |
function createPolicy(uint256 version) external; |
||
| 7 | |||
| 8 |
function freezePolicy(uint256 version) external; |
||
| 9 | |||
| 10 |
function deActivatePolicy(uint256 version) external; |
||
| 11 | |||
| 12 |
function updateEligibility( |
||
| 13 |
uint256 version, |
||
| 14 |
CreditPolicy.EligibilityCriteria calldata data |
||
| 15 |
) external; |
||
| 16 | |||
| 17 |
function updateRatios( |
||
| 18 |
uint256 version, |
||
| 19 |
CreditPolicy.FinancialRatios calldata data |
||
| 20 |
) external; |
||
| 21 | |||
| 22 |
function updateConcentration( |
||
| 23 |
uint256 version, |
||
| 24 |
CreditPolicy.ConcentrationLimits calldata data |
||
| 25 |
) external; |
||
| 26 | |||
| 27 |
function updateAttestation( |
||
| 28 |
uint256 version, |
||
| 29 |
CreditPolicy.AttestationRequirements calldata data |
||
| 30 |
) external; |
||
| 31 | |||
| 32 |
function updateCovenants( |
||
| 33 |
uint256 version, |
||
| 34 |
CreditPolicy.MaintenanceCovenants calldata data |
||
| 35 |
) external; |
||
| 36 | |||
| 37 |
function setLoanTier( |
||
| 38 |
uint256 version, |
||
| 39 |
uint8 tierId, |
||
| 40 |
CreditPolicy.LoanTier calldata tier |
||
| 41 |
) external; |
||
| 42 | |||
| 43 |
function excludeIndustry(uint256 version, bytes32 industry) external; |
||
| 44 | |||
| 45 |
function includeIndustry(uint256 version, bytes32 industry) external; |
||
| 46 | |||
| 47 |
function setPolicyDocument( |
||
| 48 |
uint256 version, |
||
| 49 |
bytes32 hash, |
||
| 50 |
string calldata uri |
||
| 51 |
) external; |
||
| 52 | |||
| 53 |
function tierExistsInPolicy( |
||
| 54 |
uint256 version, |
||
| 55 |
uint8 tierId |
||
| 56 |
) external view returns (bool); |
||
| 57 | |||
| 58 |
function changePolicyAdmin(address newAdmin) external; |
||
| 59 | |||
| 60 |
function isPolicyActive(uint256 version) external view returns (bool); |
||
| 61 | |||
| 62 |
function isPolicyFrozen(uint256 version) external view returns (bool); |
||
| 63 | |||
| 64 |
function isIndustryExcluded( |
||
| 65 |
uint256 version, |
||
| 66 |
bytes32 industry |
||
| 67 |
) external view returns (bool); |
||
| 68 |
} |
||
| 69 |
0.0%
src/interfaces/ITranchePool.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 |
import {TranchePool} from "../TranchePool.sol";
|
||
| 4 | |||
| 5 |
interface ITranchePool {
|
||
| 6 |
// logic functions |
||
| 7 | |||
| 8 |
function withdrawEquityTrancheByAmount(uint256 amount) external; |
||
| 9 | |||
| 10 |
function withdrawJuniorTrancheByAmount(uint256 amount) external; |
||
| 11 | |||
| 12 |
function withdrawSeniorTrancheByAmount(uint256 amount) external; |
||
| 13 | |||
| 14 |
function withdrawEquityTranche(uint256 shares) external; |
||
| 15 | |||
| 16 |
function withdrawJuniorTranche(uint256 shares) external; |
||
| 17 | |||
| 18 |
function withdrawSeniorTranche(uint256 shares) external; |
||
| 19 | |||
| 20 |
function onInterestAccrued( |
||
| 21 |
uint256 interestAmount, |
||
| 22 |
uint256 seniorInterest, |
||
| 23 |
uint256 juniorInterest |
||
| 24 |
) external; |
||
| 25 | |||
| 26 |
function onRepayment( |
||
| 27 |
uint256 principalRepaid, |
||
| 28 |
uint256 interestRepaid |
||
| 29 |
) external; |
||
| 30 | |||
| 31 |
function onRecovery(uint256 amount) external; |
||
| 32 | |||
| 33 |
function allocateCapital( |
||
| 34 |
uint256 totalDisbursement, |
||
| 35 |
uint256 fees, |
||
| 36 |
address deployer, |
||
| 37 |
address feeManager |
||
| 38 |
) |
||
| 39 |
external |
||
| 40 |
returns ( |
||
| 41 |
uint256 seniorAmount, |
||
| 42 |
uint256 juniorAmount, |
||
| 43 |
uint256 equityAmount |
||
| 44 |
); |
||
| 45 | |||
| 46 |
function depositEquityTranche(uint256 amount) external; |
||
| 47 | |||
| 48 |
function depositJuniorTranche(uint256 amount) external; |
||
| 49 | |||
| 50 |
function depositSeniorTranche(uint256 amount) external; |
||
| 51 | |||
| 52 |
// updaters |
||
| 53 |
function updateEquityTrancheWhiteList(address user, bool status) external; |
||
| 54 | |||
| 55 |
function updateWhitelist(address user, bool status) external; |
||
| 56 | |||
| 57 |
// setters |
||
| 58 | |||
| 59 |
function onLoss(uint256 principalLoss, uint256 interestAccrued) external; |
||
| 60 | |||
| 61 |
function setLoanEngine(address _loanEngine) external; |
||
| 62 | |||
| 63 |
function setTargetJuniorAPR(uint256 apr) external; |
||
| 64 | |||
| 65 |
function setSeniorAPR(uint256 apr) external; |
||
| 66 | |||
| 67 |
function setTrancheCapitalAllocationFactorJunior(uint256 factor) external; |
||
| 68 | |||
| 69 |
function setTrancheCapitalAllocationFactorSenior(uint256 factor) external; |
||
| 70 | |||
| 71 |
function setMinimumDepositAmountEquityTranche(uint256 amount) external; |
||
| 72 | |||
| 73 |
function setMinimumDepositAmountSeniorTranche(uint256 amount) external; |
||
| 74 | |||
| 75 |
function setMinimumDepositAmountJuniorTranche(uint256 amount) external; |
||
| 76 | |||
| 77 |
// getters |
||
| 78 |
function getEquityTrancheBalance( |
||
| 79 |
address user |
||
| 80 |
) external view returns (uint256); |
||
| 81 | |||
| 82 |
function getJuniorTrancheBalance( |
||
| 83 |
address user |
||
| 84 |
) external view returns (uint256); |
||
| 85 | |||
| 86 |
function getSeniorTrancheBalance( |
||
| 87 |
address user |
||
| 88 |
) external view returns (uint256); |
||
| 89 | |||
| 90 |
function getPoolState() external view returns (TranchePool.PoolState); |
||
| 91 | |||
| 92 |
function getSeniorAllocationRatio() external view returns (uint256); |
||
| 93 | |||
| 94 |
function getJuniorAllocationRatio() external view returns (uint256); |
||
| 95 | |||
| 96 |
function getTotalIdleValue() external view returns (uint256); |
||
| 97 |
} |
||
| 98 |
0.0%
src/interfaces/IVerifier.sol
Lines covered: 0 / 0 (0.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 | |||
| 4 |
interface IVerifier {
|
||
| 5 |
function verify( |
||
| 6 |
bytes calldata _proof, |
||
| 7 |
bytes32[] calldata _publicInputs |
||
| 8 |
) external returns (bool); |
||
| 9 |
} |
||
| 10 |
80.5%
test/medusa/MedusaTest.sol
Lines covered: 235 / 292 (80.5%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.27; |
||
| 3 | |||
| 4 |
import {LoanEngine} from "../../src/LoanEngine.sol";
|
||
| 5 |
import {TranchePool} from "../../src/TranchePool.sol";
|
||
| 6 |
import {CreditPolicy} from "../../src/CreditPolicy.sol";
|
||
| 7 |
import {MockLoanProofVerifier} from "../mocks/MockLoanProofVerifier.sol";
|
||
| 8 |
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
|
||
| 9 | |||
| 10 |
contract MedusaTest {
|
||
| 11 |
LoanEngine public loanEngine; |
||
| 12 |
TranchePool public tranchePool; |
||
| 13 |
✓ 821
|
ERC20Mock public usdt; |
|
| 14 |
✓ 2.9K
|
CreditPolicy public creditPolicy; |
|
| 15 |
|
||
| 16 |
✓ 2.3K
|
uint256 public constant USDT = 1e18; |
|
| 17 |
✓ 1
|
address public deployer = address(0x999); |
|
| 18 |
✓ 35
|
address public recevingEntity = address(0x888); |
|
| 19 |
✓ 1.1K
|
address public feeManager = address(0x777); |
|
| 20 |
|
||
| 21 |
// Configuration constants (matching Foundry handler) |
||
| 22 |
✓ 99
|
bool public allowFullDeployment = true; |
|
| 23 |
✓ 45
|
uint256 public minimumLoanPrincipal = 10_00_000 * USDT; |
|
| 24 |
✓ 31
|
uint256 public maximumLoanPrincipal = 2_00_00_000 * USDT; |
|
| 25 |
✓ 1.2K
|
uint256 public minimumOriginationFeeBps = 50; |
|
| 26 |
✓ 48
|
uint256 public minimumTermDays = 180; |
|
| 27 |
✓ 1.0K
|
uint256 public maximumTermDays = 480; |
|
| 28 |
✓ 706
|
uint256 public activePolicyVersion = 1; |
|
| 29 |
|
||
| 30 |
// Counters |
||
| 31 |
✓ 40
|
uint256 public defaultCounter; |
|
| 32 |
✓ 37
|
uint256 public writeOffCounter; |
|
| 33 |
✓ 841
|
uint256 public recoveryCounter; |
|
| 34 |
|
||
| 35 |
// Users |
||
| 36 |
✓ 37.3K
|
address[] public seniorUsers; |
|
| 37 |
✓ 32.5K
|
address[] public juniorUsers; |
|
| 38 |
✓ 43.0K
|
address[] public equityUsers; |
|
| 39 |
✓ 83.8K
|
address[] public loanBorrowers; |
|
| 40 |
|
||
| 41 |
// Ghost variables for tracking (matching Foundry handler) |
||
| 42 |
✓ 2.4K
|
uint256 public totalIdleValue; |
|
| 43 |
✓ 503
|
uint256 public totalDeployedValue; |
|
| 44 |
✓ 1.2K
|
uint256 public totalDeposited; |
|
| 45 |
✓ 32
|
uint256 public totalLoss; |
|
| 46 |
✓ 49
|
uint256 public totalRecovered; |
|
| 47 |
✓ 32
|
uint256 public totalUnclaimedInterest; |
|
| 48 |
✓ 35
|
uint256 public outStandingPrincipal; |
|
| 49 |
|
||
| 50 |
// VM cheatcodes interface |
||
| 51 |
✓ 107.1K
|
Hevm internal constant vm = Hevm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D); |
|
| 52 |
|
||
| 53 |
constructor() {
|
||
| 54 |
// Deploy contracts |
||
| 55 |
✓ 1
|
usdt = new ERC20Mock(); |
|
| 56 |
✓ 1
|
tranchePool = new TranchePool(address(usdt)); |
|
| 57 |
|
||
| 58 |
// Configure tranches (matching Foundry) |
||
| 59 |
✓ 1
|
tranchePool.setMaxAllocationCapSeniorTranche(5_00_00_000 * USDT); |
|
| 60 |
✓ 1
|
tranchePool.setMaxAllocationCapJuniorTranche(3_00_00_000 * USDT); |
|
| 61 |
✓ 1
|
tranchePool.setMaxAllocationCapEquityTranche(2_00_00_000 * USDT); |
|
| 62 |
✓ 1
|
tranchePool.setMinimumDepositAmountSeniorTranche(10_00_000 * USDT); |
|
| 63 |
✓ 1
|
tranchePool.setMinimumDepositAmountJuniorTranche(50_00_000 * USDT); |
|
| 64 |
✓ 1
|
tranchePool.setMinimumDepositAmountEquityTranche(1_00_00_000 * USDT); |
|
| 65 |
✓ 1
|
tranchePool.setTrancheCapitalAllocationFactorJunior(15); |
|
| 66 |
✓ 1
|
tranchePool.setTrancheCapitalAllocationFactorSenior(80); |
|
| 67 |
✓ 1
|
tranchePool.setSeniorAPR(8); |
|
| 68 |
✓ 1
|
tranchePool.setTargetJuniorAPR(15); |
|
| 69 |
|
||
| 70 |
// Setup credit policy |
||
| 71 |
✓ 1
|
creditPolicy = new CreditPolicy(); |
|
| 72 |
✓ 1
|
creditPolicy.setMaxTiers(3); |
|
| 73 |
✓ 1
|
creditPolicy.createPolicy(1); |
|
| 74 |
✓ 1
|
creditPolicy.updateEligibility(1, _createEligibilityCriteria()); |
|
| 75 |
✓ 1
|
creditPolicy.updateRatios(1, _createFinancialRatios()); |
|
| 76 |
✓ 1
|
creditPolicy.updateConcentration(1, _createConcentrationLimits()); |
|
| 77 |
✓ 1
|
creditPolicy.updateAttestation(1, _createAttestationRequirements()); |
|
| 78 |
✓ 1
|
creditPolicy.updateCovenants(1, _createMaintenanceCovenants()); |
|
| 79 |
✓ 1
|
creditPolicy.setLoanTier(1, 1, _createMockTier("Tier 1"));
|
|
| 80 |
✓ 1
|
creditPolicy.setPolicyDocument(1, keccak256(bytes("document")), "ipfs://policyDocHash");
|
|
| 81 |
✓ 1
|
creditPolicy.freezePolicy(1); |
|
| 82 |
|
||
| 83 |
// Setup loan engine |
||
| 84 |
✓ 1
|
MockLoanProofVerifier mockVerifier = new MockLoanProofVerifier(); |
|
| 85 |
loanEngine = new LoanEngine( |
||
| 86 |
address(creditPolicy), |
||
| 87 |
address(mockVerifier), |
||
| 88 |
✓ 1
|
500, |
|
| 89 |
address(tranchePool), |
||
| 90 |
address(usdt) |
||
| 91 |
); |
||
| 92 |
✓ 1
|
loanEngine.setMaxOriginationFeeBps(500); |
|
| 93 |
✓ 1
|
tranchePool.setLoanEngine(address(loanEngine)); |
|
| 94 |
|
||
| 95 |
// Whitelist entities |
||
| 96 |
✓ 1
|
loanEngine.setWhitelistedFeeManager(feeManager, true); |
|
| 97 |
✓ 1
|
loanEngine.setWhitelistedOffRampingEntity(recevingEntity, true); |
|
| 98 |
✓ 1
|
loanEngine.setWhitelistedRepaymentAgent(recevingEntity, true); |
|
| 99 |
✓ 1
|
loanEngine.setWhitelistedRecoveryAgent(recevingEntity, true); |
|
| 100 |
|
||
| 101 |
// Create and fund users (matching Foundry - lots of users) |
||
| 102 |
✓ 100
|
for (uint160 i = 1; i < 100; i++) {
|
|
| 103 |
✓ 99
|
seniorUsers.push(address(i)); |
|
| 104 |
✓ 99
|
if (i % 2 == 0) {
|
|
| 105 |
✓ 49
|
usdt.mint(address(i), 1_00_00_00000 * USDT); |
|
| 106 |
✓ 49
|
tranchePool.updateWhitelist(address(i), true); |
|
| 107 |
} else if (i % 3 == 0) {
|
||
| 108 |
✓ 50
|
usdt.mint(address(i), 50_0000_0000 * USDT); |
|
| 109 |
✓ 50
|
tranchePool.updateWhitelist(address(i), true); |
|
| 110 |
} else {
|
||
| 111 |
✓ 33
|
usdt.mint(address(i), 10_000_0000000 * USDT); |
|
| 112 |
tranchePool.updateWhitelist(address(i), true); |
||
| 113 |
} |
||
| 114 |
} |
||
| 115 |
|
||
| 116 |
✓ 10
|
for (uint160 i = 1; i < 10; i++) {
|
|
| 117 |
✓ 9
|
juniorUsers.push(address(i)); |
|
| 118 |
✓ 9
|
if (i % 2 == 0) {
|
|
| 119 |
✓ 4
|
usdt.mint(address(i), 500000_00_000 * USDT); |
|
| 120 |
✓ 4
|
tranchePool.updateWhitelist(address(i), true); |
|
| 121 |
} else {
|
||
| 122 |
✓ 5
|
usdt.mint(address(i), 10_000000_000 * USDT); |
|
| 123 |
✓ 5
|
tranchePool.updateWhitelist(address(i), true); |
|
| 124 |
} |
||
| 125 |
} |
||
| 126 |
|
||
| 127 |
✓ 5
|
for (uint160 i = 1; i < 5; i++) {
|
|
| 128 |
✓ 4
|
equityUsers.push(address(i)); |
|
| 129 |
✓ 4
|
usdt.mint(address(i), 50_0000_0000 * USDT); |
|
| 130 |
✓ 4
|
tranchePool.updateEquityTrancheWhiteList(address(i), true); |
|
| 131 |
} |
||
| 132 |
|
||
| 133 |
✓ 21
|
for (uint160 i = 200; i < 220; i++) {
|
|
| 134 |
✓ 20
|
loanBorrowers.push(address(i)); |
|
| 135 |
} |
||
| 136 |
|
||
| 137 |
✓ 1
|
usdt.mint(recevingEntity, 50_0000_0000 * USDT); |
|
| 138 |
} |
||
| 139 |
|
||
| 140 |
// ========================================================================= |
||
| 141 |
// FUZZ FUNCTIONS - Matching Foundry Handler Logic |
||
| 142 |
// ========================================================================= |
||
| 143 |
|
||
| 144 |
function depositSenior(uint256 userSeed, uint256 amount) external {
|
||
| 145 |
✓ 111.1K
|
if (tranchePool.getPoolState() != TranchePool.PoolState.OPEN) return; |
|
| 146 |
|
||
| 147 |
✓ 35.8K
|
address user = seniorUsers[userSeed % seniorUsers.length]; |
|
| 148 |
amount = _bound( |
||
| 149 |
amount, |
||
| 150 |
✓ 35.8K
|
tranchePool.getSeniorTrancheMinimumDepositAmount(), |
|
| 151 |
✓ 35.8K
|
tranchePool.getSeniorTrancheMaxDepositCap() |
|
| 152 |
); |
||
| 153 |
|
||
| 154 |
✓ 35.8K
|
uint256 currentValue = tranchePool.getSeniorTrancheIdleValue() + |
|
| 155 |
✓ 35.8K
|
tranchePool.getSeniorTrancheDeployedValue(); |
|
| 156 |
|
||
| 157 |
✓ 35.8K
|
if (currentValue >= tranchePool.getSeniorTrancheMaxDepositCap()) return; |
|
| 158 |
|
||
| 159 |
✓ 22.5K
|
uint256 remaining = tranchePool.getSeniorTrancheMaxDepositCap() - currentValue; |
|
| 160 |
amount = _min(amount, remaining); |
||
| 161 |
|
||
| 162 |
✓ 22.5K
|
if (amount < tranchePool.getSeniorTrancheMinimumDepositAmount()) return; |
|
| 163 |
✓ 22.5K
|
if (amount == 0) return; |
|
| 164 |
|
||
| 165 |
✓ 22.5K
|
_impersonate(user); |
|
| 166 |
✓ 22.5K
|
usdt.approve(address(tranchePool), amount); |
|
| 167 |
✓ 22.5K
|
tranchePool.depositSeniorTranche(amount); |
|
| 168 |
_stopImpersonate(); |
||
| 169 |
|
||
| 170 |
✓ 22.5K
|
totalIdleValue += amount; |
|
| 171 |
} |
||
| 172 |
|
||
| 173 |
function depositJunior(uint256 userSeed, uint256 amount) external {
|
||
| 174 |
✓ 112.9K
|
if (tranchePool.getPoolState() != TranchePool.PoolState.OPEN) return; |
|
| 175 |
|
||
| 176 |
✓ 31.7K
|
address user = juniorUsers[userSeed % juniorUsers.length]; |
|
| 177 |
amount = _bound( |
||
| 178 |
amount, |
||
| 179 |
✓ 31.7K
|
tranchePool.getJuniorTrancheMinimumDepositAmount(), |
|
| 180 |
✓ 31.7K
|
tranchePool.getJuniorTrancheMaxDepositCap() |
|
| 181 |
); |
||
| 182 |
|
||
| 183 |
✓ 31.7K
|
uint256 currentValue = tranchePool.getJuniorTrancheIdleValue() + |
|
| 184 |
✓ 31.7K
|
tranchePool.getJuniorTrancheDeployedValue(); |
|
| 185 |
|
||
| 186 |
✓ 31.7K
|
if (currentValue >= tranchePool.getJuniorTrancheMaxDepositCap()) return; |
|
| 187 |
|
||
| 188 |
✓ 22.3K
|
uint256 remaining = tranchePool.getJuniorTrancheMaxDepositCap() - currentValue; |
|
| 189 |
amount = _min(amount, remaining); |
||
| 190 |
|
||
| 191 |
✓ 22.3K
|
if (amount < tranchePool.getJuniorTrancheMinimumDepositAmount()) return; |
|
| 192 |
✓ 22.3K
|
if (amount == 0) return; |
|
| 193 |
|
||
| 194 |
✓ 22.3K
|
_impersonate(user); |
|
| 195 |
✓ 22.3K
|
usdt.approve(address(tranchePool), amount); |
|
| 196 |
✓ 67.1K
|
tranchePool.depositJuniorTranche(amount); |
|
| 197 |
_stopImpersonate(); |
||
| 198 |
|
||
| 199 |
✓ 67.1K
|
totalIdleValue += amount; |
|
| 200 |
} |
||
| 201 |
|
||
| 202 |
function depositEquity(uint256 userSeed, uint256 amount) external {
|
||
| 203 |
✓ 111.5K
|
if (tranchePool.getPoolState() != TranchePool.PoolState.OPEN) return; |
|
| 204 |
|
||
| 205 |
✓ 43.0K
|
address user = equityUsers[userSeed % equityUsers.length]; |
|
| 206 |
amount = _bound( |
||
| 207 |
amount, |
||
| 208 |
✓ 43.0K
|
tranchePool.getEquityTrancheMinimumDepositAmount(), |
|
| 209 |
✓ 43.0K
|
tranchePool.getEquityTrancheMaxDepositCap() |
|
| 210 |
); |
||
| 211 |
|
||
| 212 |
✓ 43.0K
|
uint256 currentValue = tranchePool.getEquityTrancheIdleValue() + |
|
| 213 |
✓ 43.0K
|
tranchePool.getEquityTrancheDeployedValue(); |
|
| 214 |
|
||
| 215 |
✓ 43.0K
|
if (currentValue >= tranchePool.getEquityTrancheMaxDepositCap()) return; |
|
| 216 |
|
||
| 217 |
✓ 22.3K
|
uint256 remaining = tranchePool.getEquityTrancheMaxDepositCap() - currentValue; |
|
| 218 |
amount = _min(amount, remaining); |
||
| 219 |
|
||
| 220 |
✓ 22.3K
|
if (amount < tranchePool.getEquityTrancheMinimumDepositAmount()) return; |
|
| 221 |
✓ 22.2K
|
if (amount == 0) return; |
|
| 222 |
|
||
| 223 |
✓ 22.2K
|
_impersonate(user); |
|
| 224 |
✓ 22.2K
|
usdt.approve(address(tranchePool), amount); |
|
| 225 |
✓ 22.2K
|
tranchePool.depositEquityTranche(amount); |
|
| 226 |
_stopImpersonate(); |
||
| 227 |
|
||
| 228 |
✓ 22.2K
|
totalIdleValue += amount; |
|
| 229 |
} |
||
| 230 |
|
||
| 231 |
function commitPool() external {
|
||
| 232 |
✓ 26.5K
|
if (tranchePool.getPoolState() == TranchePool.PoolState.OPEN && |
|
| 233 |
✓ 34.8K
|
tranchePool.getTotalIdleValue() > 0) {
|
|
| 234 |
✓ 26.5K
|
tranchePool.setPoolState(TranchePool.PoolState.COMMITED); |
|
| 235 |
✓ 26.5K
|
totalDeposited = tranchePool.getTotalIdleValue(); |
|
| 236 |
} |
||
| 237 |
} |
||
| 238 |
|
||
| 239 |
function createLoan( |
||
| 240 |
uint256 principalIssued, |
||
| 241 |
uint256 originationFeeBps, |
||
| 242 |
uint256 termDays, |
||
| 243 |
uint256 userIndex |
||
| 244 |
) external {
|
||
| 245 |
✓ 121.2K
|
if (tranchePool.getPoolState() != TranchePool.PoolState.COMMITED && |
|
| 246 |
✓ 78.9K
|
tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED) return; |
|
| 247 |
|
||
| 248 |
// Matching Foundry handler logic |
||
| 249 |
✓ 83.2K
|
uint256 minPrincipal = minimumLoanPrincipal; |
|
| 250 |
|
||
| 251 |
✓ 83.2K
|
if (allowFullDeployment) {
|
|
| 252 |
✓ 83.2K
|
if (tranchePool.getTotalIdleValue() < minPrincipal) {
|
|
| 253 |
✓ 287
|
minPrincipal = tranchePool.getTotalIdleValue(); |
|
| 254 |
} |
||
| 255 |
} else {
|
||
| 256 |
if (tranchePool.getTotalIdleValue() < minPrincipal * 10) {
|
||
| 257 |
return; |
||
| 258 |
} |
||
| 259 |
} |
||
| 260 |
|
||
| 261 |
✓ 83.2K
|
if (tranchePool.getTotalIdleValue() < minimumLoanPrincipal) return; |
|
| 262 |
|
||
| 263 |
principalIssued = _bound( |
||
| 264 |
principalIssued, |
||
| 265 |
minimumLoanPrincipal, |
||
| 266 |
✓ 82.9K
|
_min(maximumLoanPrincipal, tranchePool.getTotalIdleValue()) |
|
| 267 |
); |
||
| 268 |
|
||
| 269 |
✓ 82.9K
|
if (principalIssued > tranchePool.getTotalIdleValue() / 10) {
|
|
| 270 |
✓ 63.2K
|
principalIssued = tranchePool.getTotalIdleValue() / 10; |
|
| 271 |
} |
||
| 272 |
|
||
| 273 |
originationFeeBps = _bound( |
||
| 274 |
originationFeeBps, |
||
| 275 |
✓ 82.9K
|
minimumOriginationFeeBps, |
|
| 276 |
✓ 82.9K
|
loanEngine.getMaxOriginationFeeBps() |
|
| 277 |
); |
||
| 278 |
|
||
| 279 |
✓ 82.9K
|
termDays = _bound(termDays, minimumTermDays, maximumTermDays); |
|
| 280 |
|
||
| 281 |
✓ 82.9K
|
if (!creditPolicy.isPolicyFrozen(activePolicyVersion)) return; |
|
| 282 |
|
||
| 283 |
bytes32 borrowerCommitment = keccak256( |
||
| 284 |
abi.encodePacked( |
||
| 285 |
✓ 82.9K
|
loanBorrowers[userIndex % loanBorrowers.length], |
|
| 286 |
userIndex |
||
| 287 |
) |
||
| 288 |
); |
||
| 289 |
|
||
| 290 |
✓ 82.9K
|
uint256 nextLoanId = loanEngine.getNextLoanId(); |
|
| 291 |
✓ 82.9K
|
bytes memory proofData = abi.encodePacked( |
|
| 292 |
nextLoanId, |
||
| 293 |
userIndex, |
||
| 294 |
principalIssued, |
||
| 295 |
originationFeeBps, |
||
| 296 |
termDays |
||
| 297 |
); |
||
| 298 |
|
||
| 299 |
loanEngine.createLoan( |
||
| 300 |
borrowerCommitment, |
||
| 301 |
✓ 82.9K
|
keccak256(abi.encode(nextLoanId, userIndex, borrowerCommitment, block.timestamp)), |
|
| 302 |
activePolicyVersion, |
||
| 303 |
1, |
||
| 304 |
principalIssued, |
||
| 305 |
✓ 82.9K
|
500, |
|
| 306 |
originationFeeBps, |
||
| 307 |
termDays, |
||
| 308 |
bytes32(0), |
||
| 309 |
proofData, |
||
| 310 |
new bytes32[](0) |
||
| 311 |
); |
||
| 312 |
} |
||
| 313 |
|
||
| 314 |
function activateLoan(uint256 loanId) external {
|
||
| 315 |
✓ 149.9K
|
if (loanEngine.getNextLoanId() == 1) return; |
|
| 316 |
|
||
| 317 |
✓ 64.8K
|
loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1); |
|
| 318 |
|
||
| 319 |
✓ 64.8K
|
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId); |
|
| 320 |
✓ 64.8K
|
if (loan.state != LoanEngine.LoanState.CREATED) return; |
|
| 321 |
|
||
| 322 |
✓ 36.1K
|
if (tranchePool.getPoolState() != TranchePool.PoolState.COMMITED && |
|
| 323 |
✓ 134.0K
|
tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED) return; |
|
| 324 |
|
||
| 325 |
✓ 35.3K
|
if (loan.principalIssued > tranchePool.getTotalIdleValue()) return; |
|
| 326 |
|
||
| 327 |
✓ 35.3K
|
loanEngine.activateLoan(loanId, recevingEntity, feeManager); |
|
| 328 |
|
||
| 329 |
✓ 35.3K
|
totalDeployedValue += loan.principalIssued; |
|
| 330 |
✓ 35.3K
|
totalIdleValue -= loan.principalIssued; |
|
| 331 |
✓ 52.5K
|
outStandingPrincipal += loan.principalIssued; |
|
| 332 |
} |
||
| 333 |
|
||
| 334 |
function repayLoan(uint256 loanId, uint256 principalAmount, uint256 interestAmount) external {
|
||
| 335 |
✓ 107.8K
|
if (loanEngine.getNextLoanId() == 1) return; |
|
| 336 |
|
||
| 337 |
✓ 56.2K
|
loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1); |
|
| 338 |
|
||
| 339 |
✓ 56.2K
|
LoanEngine.Loan memory loanDetails = loanEngine.getLoanDetails(loanId); |
|
| 340 |
✓ 56.2K
|
if (loanDetails.state != LoanEngine.LoanState.ACTIVE) return; |
|
| 341 |
|
||
| 342 |
✓ 17.9K
|
principalAmount = _bound(principalAmount, 0, loanDetails.principalOutstanding); |
|
| 343 |
|
||
| 344 |
✓ 17.9K
|
uint256 pendingInterest = _accrueInterest(loanId); |
|
| 345 |
✓ 17.9K
|
uint256 totalInterestDue = loanDetails.interestAccrued + pendingInterest; |
|
| 346 |
|
||
| 347 |
✓ 17.9K
|
interestAmount = _bound(interestAmount, 0, totalInterestDue); |
|
| 348 |
|
||
| 349 |
✓ 17.9K
|
if (principalAmount == 0 && interestAmount == 0) return; |
|
| 350 |
|
||
| 351 |
// Interest before principal rule |
||
| 352 |
✓ 38.3K
|
if (principalAmount > 0 && interestAmount == 0 && totalInterestDue > 0) return; |
|
| 353 |
|
||
| 354 |
✓ 17.2K
|
uint256 totalRepayAmount = principalAmount + interestAmount; |
|
| 355 |
|
||
| 356 |
// Calculate ACTUAL amounts that will be paid (matching Foundry) |
||
| 357 |
✓ 21.0K
|
uint256 interestAccrued = loanDetails.interestAccrued + _accrueInterest(loanId); |
|
| 358 |
uint256 actualInterestPaid = _min(totalRepayAmount, interestAccrued); |
||
| 359 |
uint256 actualPrincipalPaid = _min( |
||
| 360 |
✓ 17.2K
|
totalRepayAmount - actualInterestPaid, |
|
| 361 |
loanDetails.principalOutstanding |
||
| 362 |
); |
||
| 363 |
|
||
| 364 |
✓ 17.2K
|
_impersonate(recevingEntity); |
|
| 365 |
✓ 17.2K
|
usdt.approve(address(loanEngine), totalRepayAmount); |
|
| 366 |
_stopImpersonate(); |
||
| 367 |
|
||
| 368 |
✓ 17.2K
|
totalUnclaimedInterest += actualInterestPaid; |
|
| 369 |
|
||
| 370 |
✓ 17.2K
|
loanEngine.repayLoan(loanId, principalAmount, interestAmount, recevingEntity); |
|
| 371 |
|
||
| 372 |
// Use ACTUAL principal paid (THIS IS THE FIX!) |
||
| 373 |
✓ 17.2K
|
totalDeployedValue -= actualPrincipalPaid; |
|
| 374 |
✓ 17.2K
|
totalIdleValue += actualPrincipalPaid; |
|
| 375 |
✓ 17.2K
|
outStandingPrincipal -= actualPrincipalPaid; |
|
| 376 |
} |
||
| 377 |
|
||
| 378 |
function warpTime(uint256 daysToWarp) external {
|
||
| 379 |
✓ 107.1K
|
daysToWarp = _bound(daysToWarp, 1, 365); |
|
| 380 |
✓ 107.1K
|
vm.warp(block.timestamp + (daysToWarp * 1 days)); |
|
| 381 |
} |
||
| 382 |
|
||
| 383 |
function maybeDeclareDefault(uint256 loanId, bytes32 reasonHash) external {
|
||
| 384 |
✓ 254.7K
|
if (loanEngine.getNextLoanId() == 1) return; |
|
| 385 |
|
||
| 386 |
✓ 61.4K
|
defaultCounter++; |
|
| 387 |
✓ 61.4K
|
if (defaultCounter % 10 != 0) return; |
|
| 388 |
|
||
| 389 |
✓ 971
|
loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1); |
|
| 390 |
|
||
| 391 |
✓ 971
|
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId); |
|
| 392 |
✓ 479.3K
|
if (loan.state != LoanEngine.LoanState.ACTIVE) return; |
|
| 393 |
|
||
| 394 |
✓ 254.7K
|
loanEngine.declareDefault(loanId, reasonHash); |
|
| 395 |
} |
||
| 396 |
|
||
| 397 |
function maybeWriteOffLoan(uint256 loanId) external {
|
||
| 398 |
✓ 102.1K
|
if (loanEngine.getNextLoanId() == 1) return; |
|
| 399 |
|
||
| 400 |
✓ 54.6K
|
writeOffCounter++; |
|
| 401 |
|
||
| 402 |
✓ 54.6K
|
loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1); |
|
| 403 |
|
||
| 404 |
✓ 54.6K
|
if (loanEngine.getLoanDetails(loanId).state != LoanEngine.LoanState.DEFAULTED) return; |
|
| 405 |
|
||
| 406 |
// Read principal before writeoff |
||
| 407 |
✓ 5
|
uint256 principalOutstanding = loanEngine.getLoanDetails(loanId).principalOutstanding; |
|
| 408 |
|
||
| 409 |
✓ 5
|
loanEngine.writeOffLoan(loanId); |
|
| 410 |
|
||
| 411 |
✓ 5
|
totalDeployedValue -= principalOutstanding; |
|
| 412 |
✓ 5
|
outStandingPrincipal -= principalOutstanding; |
|
| 413 |
✓ 5
|
totalLoss += principalOutstanding; |
|
| 414 |
} |
||
| 415 |
|
||
| 416 |
function maybeRecoverLoan(uint256 loanId, uint256 amount, uint256 agentIndex) external {
|
||
| 417 |
✓ 107.7K
|
if (loanEngine.getNextLoanId() == 1) return; |
|
| 418 |
|
||
| 419 |
✓ 54.2K
|
recoveryCounter++; |
|
| 420 |
|
||
| 421 |
✓ 54.2K
|
loanId = _bound(loanId, 1, loanEngine.getNextLoanId() - 1); |
|
| 422 |
|
||
| 423 |
✓ 54.2K
|
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId); |
|
| 424 |
✓ 54.2K
|
if (loan.state != LoanEngine.LoanState.WRITTEN_OFF) return; |
|
| 425 |
|
||
| 426 |
amount = _bound(amount, 1, loan.principalIssued); |
||
| 427 |
|
||
| 428 |
_impersonate(recevingEntity); |
||
| 429 |
usdt.approve(address(loanEngine), amount); |
||
| 430 |
_stopImpersonate(); |
||
| 431 |
|
||
| 432 |
loanEngine.recoverLoan(loanId, amount, recevingEntity); |
||
| 433 |
|
||
| 434 |
totalIdleValue += amount; |
||
| 435 |
totalRecovered += amount; |
||
| 436 |
} |
||
| 437 |
|
||
| 438 |
function mayClosePool() external {
|
||
| 439 |
✓ 101.5K
|
if (tranchePool.getTotalDeployedValue() > 0 || |
|
| 440 |
✓ 98.4K
|
tranchePool.getPoolState() != TranchePool.PoolState.DEPLOYED) return; |
|
| 441 |
|
||
| 442 |
✓ 3.1K
|
tranchePool.setPoolState(TranchePool.PoolState.CLOSED); |
|
| 443 |
} |
||
| 444 |
|
||
| 445 |
// ========================================================================= |
||
| 446 |
// INVARIANTS - Matching Foundry Invariants |
||
| 447 |
// ========================================================================= |
||
| 448 |
|
||
| 449 |
function invariant_totalValueBalance() external view returns (bool) {
|
||
| 450 |
✓ 1.2M
|
return (tranchePool.getTotalIdleValue() + tranchePool.getTotalDeployedValue()) == |
|
| 451 |
(tranchePool.getTotalDeposited() - tranchePool.getTotalLoss() + tranchePool.getTotalRecovered()); |
||
| 452 |
} |
||
| 453 |
|
||
| 454 |
function invariant_deployedMatchesOutstanding() external view returns (bool) {
|
||
| 455 |
✓ 8.4K
|
return outStandingPrincipal == tranchePool.getTotalDeployedValue(); |
|
| 456 |
} |
||
| 457 |
|
||
| 458 |
function invariant_principalIntegrity() external view returns (bool) {
|
||
| 459 |
uint256 totalPrincipal = 0; |
||
| 460 |
✓ 8.5K
|
uint256 nextId = loanEngine.getNextLoanId(); |
|
| 461 |
|
||
| 462 |
for (uint256 i = 1; i < nextId; i++) {
|
||
| 463 |
✓ 265.9K
|
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(i); |
|
| 464 |
totalPrincipal += loan.principalOutstanding; |
||
| 465 |
} |
||
| 466 |
|
||
| 467 |
return totalPrincipal == tranchePool.getTotalDeployedValue(); |
||
| 468 |
} |
||
| 469 | |||
| 470 |
function invariant_tokenBalance() external view returns (bool) {
|
||
| 471 |
✓ 1.2M
|
return (tranchePool.getTotalUnclaimedInterest() + |
|
| 472 |
tranchePool.getTotalIdleValue() + |
||
| 473 |
tranchePool.getProtocolRevenue()) == |
||
| 474 |
usdt.balanceOf(address(tranchePool)); |
||
| 475 |
} |
||
| 476 | |||
| 477 |
function invariant_trancheSum() external view returns (bool) {
|
||
| 478 |
✓ 562.6K
|
return tranchePool.getTotalDeployedValue() == |
|
| 479 |
(tranchePool.getSeniorTrancheDeployedValue() + |
||
| 480 |
tranchePool.getJuniorTrancheDeployedValue() + |
||
| 481 |
tranchePool.getEquityTrancheDeployedValue()); |
||
| 482 |
} |
||
| 483 | |||
| 484 |
function invariant_waterfallSymmetry() external view returns (bool) {
|
||
| 485 |
uint256 totalShortfall = tranchePool.getSeniorPrincipalShortfall() + |
||
| 486 |
tranchePool.getJuniorPrincipalShortfall() + |
||
| 487 |
tranchePool.getEquityPrincipalShortfall(); |
||
| 488 | |||
| 489 |
if (tranchePool.getTotalRecovered() >= tranchePool.getTotalLoss()) {
|
||
| 490 |
return totalShortfall == 0; |
||
| 491 |
} else {
|
||
| 492 |
return totalShortfall == (tranchePool.getTotalLoss() - tranchePool.getTotalRecovered()); |
||
| 493 |
} |
||
| 494 |
} |
||
| 495 | |||
| 496 |
function invariant_idleIntegrity() external view returns (bool) {
|
||
| 497 |
✓ 461.1K
|
return (tranchePool.getSeniorTrancheIdleValue() + |
|
| 498 |
tranchePool.getJuniorTrancheIdleValue() + |
||
| 499 |
tranchePool.getEquityTrancheIdleValue()) == |
||
| 500 |
tranchePool.getTotalIdleValue(); |
||
| 501 |
} |
||
| 502 | |||
| 503 |
function invariant_seniorShareOpen() external view returns (bool) {
|
||
| 504 |
if (tranchePool.getPoolState() == TranchePool.PoolState.OPEN) {
|
||
| 505 |
return tranchePool.getTotalSeniorShares() == tranchePool.getSeniorTrancheIdleValue(); |
||
| 506 |
} |
||
| 507 |
return true; |
||
| 508 |
} |
||
| 509 | |||
| 510 |
function invariant_loanState() external view returns (bool) {
|
||
| 511 |
uint256 nextId = loanEngine.getNextLoanId(); |
||
| 512 |
for (uint256 i = 1; i < nextId; i++) {
|
||
| 513 |
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(i); |
||
| 514 |
|
||
| 515 |
if (loan.state == LoanEngine.LoanState.NONE || |
||
| 516 |
loan.state == LoanEngine.LoanState.CREATED) {
|
||
| 517 |
if (loan.principalOutstanding != 0) return false; |
||
| 518 |
} |
||
| 519 |
|
||
| 520 |
if (loan.state == LoanEngine.LoanState.REPAID || |
||
| 521 |
loan.state == LoanEngine.LoanState.WRITTEN_OFF) {
|
||
| 522 |
if (loan.principalOutstanding != 0) return false; |
||
| 523 |
} |
||
| 524 |
|
||
| 525 |
if (loan.state == LoanEngine.LoanState.ACTIVE) {
|
||
| 526 |
if (loan.principalOutstanding > loan.principalIssued) return false; |
||
| 527 |
} |
||
| 528 |
} |
||
| 529 |
return true; |
||
| 530 |
} |
||
| 531 | |||
| 532 |
function invariant_interestMonotonicity() external view returns (bool) {
|
||
| 533 |
✓ 8.5K
|
return tranchePool.getSeniorInterestIndex() >= 1e18 && |
|
| 534 |
tranchePool.getJuniorInterestIndex() >= 1e18 && |
||
| 535 |
tranchePool.getEquityInterestIndex() >= 1e18; |
||
| 536 |
} |
||
| 537 | |||
| 538 |
function invariant_poolState() external view returns (bool) {
|
||
| 539 |
✓ 1.7K
|
TranchePool.PoolState state = tranchePool.getPoolState(); |
|
| 540 |
if (state == TranchePool.PoolState.OPEN || |
||
| 541 |
state == TranchePool.PoolState.CLOSED) {
|
||
| 542 |
if (tranchePool.getTotalDeployedValue() != 0) return false; |
||
| 543 |
} |
||
| 544 |
✓ 1.7K
|
return true; |
|
| 545 |
} |
||
| 546 | |||
| 547 |
function invariant_interestAccounting() external view returns (bool) {
|
||
| 548 |
uint256 nextId = loanEngine.getNextLoanId(); |
||
| 549 |
for (uint256 i = 1; i < nextId; i++) {
|
||
| 550 |
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(i); |
||
| 551 |
if (loan.state == LoanEngine.LoanState.REPAID) {
|
||
| 552 |
if (loan.interestAccrued != 0) return false; |
||
| 553 |
} |
||
| 554 |
if (loan.state == LoanEngine.LoanState.WRITTEN_OFF) {
|
||
| 555 |
if (loan.interestAccrued != 0) return false; |
||
| 556 |
} |
||
| 557 |
} |
||
| 558 |
return true; |
||
| 559 |
} |
||
| 560 |
|
||
| 561 |
// ========================================================================= |
||
| 562 |
// HELPER FUNCTIONS |
||
| 563 |
// ========================================================================= |
||
| 564 |
|
||
| 565 |
function _accrueInterest(uint256 loanId) internal view returns (uint256) {
|
||
| 566 |
✓ 35.2K
|
LoanEngine.Loan memory loan = loanEngine.getLoanDetails(loanId); |
|
| 567 |
|
||
| 568 |
✓ 35.2K
|
uint256 timeElapsed = block.timestamp - loan.lastAccrualTimestamp; |
|
| 569 |
✓ 35.2K
|
if (loan.principalOutstanding == 0) return 0; |
|
| 570 |
|
||
| 571 |
✓ 35.2K
|
uint256 interest = (loan.principalOutstanding * loan.aprBps * timeElapsed) / (365 days * 10_000); |
|
| 572 |
return interest; |
||
| 573 |
} |
||
| 574 |
|
||
| 575 |
function _createEligibilityCriteria() internal pure returns (CreditPolicy.EligibilityCriteria memory) {
|
||
| 576 |
✓ 1
|
return CreditPolicy.EligibilityCriteria({
|
|
| 577 |
minAnnualRevenue: 1_00_00_000, |
||
| 578 |
minEBITDA: 10_00_000, |
||
| 579 |
minTangibleNetWorth: 5_00_00_000, |
||
| 580 |
minBusinessAgeDays: 180, |
||
| 581 |
maxDefaultsLast36Months: 0, |
||
| 582 |
bankruptcyExcluded: true |
||
| 583 |
}); |
||
| 584 |
} |
||
| 585 |
|
||
| 586 |
function _createFinancialRatios() internal pure returns (CreditPolicy.FinancialRatios memory) {
|
||
| 587 |
return CreditPolicy.FinancialRatios({
|
||
| 588 |
✓ 1
|
maxTotalDebtToEBITDA: 4e18, |
|
| 589 |
✓ 1
|
minInterestCoverageRatio: 2e18, |
|
| 590 |
minCurrentRatio: 1e18, |
||
| 591 |
✓ 1
|
minEBITDAMarginBps: 1500 |
|
| 592 |
}); |
||
| 593 |
} |
||
| 594 |
|
||
| 595 |
function _createConcentrationLimits() internal pure returns (CreditPolicy.ConcentrationLimits memory) {
|
||
| 596 |
return CreditPolicy.ConcentrationLimits({
|
||
| 597 |
✓ 1
|
maxSingleBorrowerBps: 1000, |
|
| 598 |
✓ 1
|
maxIndustryConcentrationBps: 3000 |
|
| 599 |
}); |
||
| 600 |
} |
||
| 601 |
|
||
| 602 |
function _createAttestationRequirements() internal pure returns (CreditPolicy.AttestationRequirements memory) {
|
||
| 603 |
return CreditPolicy.AttestationRequirements({
|
||
| 604 |
✓ 1
|
maxAttestationAgeDays: 90, |
|
| 605 |
reAttestationFrequencyDays: 180, |
||
| 606 |
requiresCPAAttestation: true |
||
| 607 |
}); |
||
| 608 |
} |
||
| 609 |
|
||
| 610 |
function _createMaintenanceCovenants() internal pure returns (CreditPolicy.MaintenanceCovenants memory) {
|
||
| 611 |
✓ 1
|
return CreditPolicy.MaintenanceCovenants({
|
|
| 612 |
maxLeverageRatio: 4e18, |
||
| 613 |
minCoverageRatio: 2e18, |
||
| 614 |
minLiquidityAmount: 1_00_00_000, |
||
| 615 |
allowsDividends: false, |
||
| 616 |
reportingFrequencyDays: 90 |
||
| 617 |
}); |
||
| 618 |
} |
||
| 619 |
|
||
| 620 |
function _createMockTier(string memory name) internal pure returns (CreditPolicy.LoanTier memory) {
|
||
| 621 |
return CreditPolicy.LoanTier({
|
||
| 622 |
name: name, |
||
| 623 |
minRevenue: 1_00_00_000, |
||
| 624 |
maxRevenue: 5_00_00_000, |
||
| 625 |
minEBITDA: 10_00_000, |
||
| 626 |
✓ 1
|
maxDebtToEBITDA: 3e18, |
|
| 627 |
maxLoanToEBITDA: 2e18, |
||
| 628 |
✓ 1
|
interestRateBps: 800, |
|
| 629 |
originationFeeBps: 100, |
||
| 630 |
✓ 1
|
termDays: 365, |
|
| 631 |
active: true |
||
| 632 |
}); |
||
| 633 |
} |
||
| 634 |
|
||
| 635 |
function _bound(uint256 x, uint256 min, uint256 max) internal pure returns (uint256) {
|
||
| 636 |
✓ 359.2K
|
if (min > max) return min; |
|
| 637 |
✓ 359.2K
|
if (x < min) return min; |
|
| 638 |
✓ 298.3K
|
if (x > max) return max; |
|
| 639 |
✓ 9.7K
|
return min + (x % (max - min + 1)); |
|
| 640 |
} |
||
| 641 |
|
||
| 642 |
function _min(uint256 a, uint256 b) internal pure returns (uint256) {
|
||
| 643 |
✓ 82.9K
|
return a < b ? a : b; |
|
| 644 |
} |
||
| 645 |
|
||
| 646 |
// Medusa impersonate pattern |
||
| 647 |
function _impersonate(address who) internal {
|
||
| 648 |
✓ 84.3K
|
vm.startPrank(who); |
|
| 649 |
} |
||
| 650 |
|
||
| 651 |
function _stopImpersonate() internal {
|
||
| 652 |
✓ 84.3K
|
vm.stopPrank(); |
|
| 653 |
} |
||
| 654 |
} |
||
| 655 | |||
| 656 |
// Hevm interface for cheatcodes |
||
| 657 |
interface Hevm {
|
||
| 658 |
function startPrank(address) external; |
||
| 659 |
function stopPrank() external; |
||
| 660 |
function warp(uint256) external; |
||
| 661 |
} |
100.0%
test/mocks/MockLoanProofVerifier.sol
Lines covered: 2 / 2 (100.0%)
| 1 |
// SPDX-License-Identifier: MIT |
||
| 2 |
pragma solidity ^0.8.24; |
||
| 3 | |||
| 4 |
✓ 1
|
contract MockLoanProofVerifier {
|
|
| 5 |
function verify( |
||
| 6 |
bytes calldata proof, |
||
| 7 |
bytes32[] calldata publicInputs |
||
| 8 |
) external pure returns (bool) {
|
||
| 9 |
✓ 82.9K
|
return true; |
|
| 10 |
} |
||
| 11 |
} |
||
| 12 |